From dbe71c3be4023161a5ff05c1072790f4d81ea14b Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Mon, 1 Apr 2024 21:20:00 +0100 Subject: [PATCH 1/7] Improve test coverage --- .gitignore | 1 + app/classes.py | 10 +- app/config.py | 4 +- app/helpers.py | 10 +- app/infotabs.py | 8 +- app/models.py | 28 ++- app/musicmuster.py | 20 +-- app/playlistmodel.py | 22 ++- archive/audplayer.py | 8 +- poetry.lock | 89 ++++++++- pyproject.toml | 1 + test_helpers.py => tests/test_helpers.py | 10 +- tests/test_misc.py | 25 +++ test_models.py => tests/test_models.py | 170 ++++++++++++------ .../test_playlistmodel.py | 21 ++- 15 files changed, 304 insertions(+), 123 deletions(-) rename test_helpers.py => tests/test_helpers.py (87%) create mode 100644 tests/test_misc.py rename test_models.py => tests/test_models.py (52%) rename test_playlistmodel.py => tests/test_playlistmodel.py (96%) diff --git a/.gitignore b/.gitignore index f7f3b10..cf03c11 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ StudioPlaylist.png *.howto .direnv tmp/ +.coverage diff --git a/app/classes.py b/app/classes.py index 0cd1c93..35edb26 100644 --- a/app/classes.py +++ b/app/classes.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from datetime import datetime, timedelta +import datetime as dt from typing import Optional from PyQt6.QtCore import pyqtSignal, QObject, QThread @@ -108,7 +108,7 @@ class PlaylistTrack: self.artist: Optional[str] = None self.duration: Optional[int] = None - self.end_time: Optional[datetime] = None + self.end_time: Optional[dt.datetime] = None self.fade_at: Optional[int] = None self.fade_graph: Optional[FadeCurve] = None self.fade_length: Optional[int] = None @@ -119,7 +119,7 @@ class PlaylistTrack: self.resume_marker: Optional[float] = None self.silence_at: Optional[int] = None self.start_gap: Optional[int] = None - self.start_time: Optional[datetime] = None + self.start_time: Optional[dt.datetime] = None self.title: Optional[str] = None self.track_id: Optional[int] = None @@ -177,9 +177,9 @@ class PlaylistTrack: Called when track starts playing """ - self.start_time = datetime.now() + self.start_time = dt.datetime.now() if self.duration: - self.end_time = self.start_time + timedelta(milliseconds=self.duration) + self.end_time = self.start_time + dt.timedelta(milliseconds=self.duration) class AddFadeCurve(QObject): diff --git a/app/config.py b/app/config.py index 563f221..62c5d5f 100644 --- a/app/config.py +++ b/app/config.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt import logging import os from typing import List, Optional @@ -38,7 +38,7 @@ class Config(object): DEBUG_MODULES: List[Optional[str]] = ["dbconfig"] DEFAULT_COLUMN_WIDTH = 200 DISPLAY_SQL = False - EPOCH = datetime.datetime(1970, 1, 1) + EPOCH = dt.datetime(1970, 1, 1) ERRORS_FROM = ["noreply@midnighthax.com"] ERRORS_TO = ["kae@midnighthax.com"] FADE_CURVE_BACKGROUND = "lightyellow" diff --git a/app/helpers.py b/app/helpers.py index 3fc7433..eff7588 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -1,4 +1,4 @@ -from datetime import datetime +import datetime as dt from email.message import EmailMessage from typing import Any, Dict, Optional import functools @@ -99,7 +99,7 @@ def get_audio_segment(path: str) -> Optional[AudioSegment]: return None -def get_embedded_time(text: str) -> Optional[datetime]: +def get_embedded_time(text: str) -> Optional[dt.datetime]: """Return datetime specified as @hh:mm in text""" try: @@ -110,7 +110,7 @@ def get_embedded_time(text: str) -> Optional[datetime]: return None try: - return datetime.strptime(match.group(0)[1:], Config.NOTE_TIME_FORMAT) + return dt.datetime.strptime(match.group(0)[1:], Config.NOTE_TIME_FORMAT) except ValueError: return None @@ -143,7 +143,7 @@ def get_file_metadata(filepath: str) -> dict: def get_relative_date( - past_date: Optional[datetime], reference_date: Optional[datetime] = None + past_date: Optional[dt.datetime], reference_date: Optional[dt.datetime] = None ) -> str: """ Return how long before reference_date past_date is as string. @@ -158,7 +158,7 @@ def get_relative_date( if not past_date or past_date == Config.EPOCH: return "Never" if not reference_date: - reference_date = datetime.now() + reference_date = dt.datetime.now() # Check parameters if past_date > reference_date: diff --git a/app/infotabs.py b/app/infotabs.py index b01907f..37c596a 100644 --- a/app/infotabs.py +++ b/app/infotabs.py @@ -1,6 +1,6 @@ import urllib.parse -from datetime import datetime +import datetime as dt from slugify import slugify # type: ignore from typing import Dict from PyQt6.QtCore import QUrl # type: ignore @@ -24,14 +24,14 @@ class InfoTabs(QTabWidget): self.signals.search_songfacts_signal.connect(self.open_in_songfacts) self.signals.search_wikipedia_signal.connect(self.open_in_wikipedia) # re-use the oldest one later) - self.last_update: Dict[QWebEngineView, datetime] = {} + self.last_update: Dict[QWebEngineView, dt.datetime] = {} self.tabtitles: Dict[int, str] = {} # Create one tab which (for some reason) creates flickering if # done later widget = QWebEngineView() widget.setZoomFactor(Config.WEB_ZOOM_FACTOR) - self.last_update[widget] = datetime.now() + self.last_update[widget] = dt.datetime.now() _ = self.addTab(widget, "") def open_in_songfacts(self, title): @@ -80,7 +80,7 @@ class InfoTabs(QTabWidget): self.setTabText(tab_index, short_title) widget.setUrl(QUrl(url)) - self.last_update[widget] = datetime.now() + self.last_update[widget] = dt.datetime.now() self.tabtitles[tab_index] = url # Show newly updated tab diff --git a/app/models.py b/app/models.py index 3db0fac..56d186d 100644 --- a/app/models.py +++ b/app/models.py @@ -5,8 +5,7 @@ import re from config import Config from dbconfig import scoped_session -from datetime import datetime -from pprint import pprint +import datetime as dt from typing import List, Optional, Sequence from sqlalchemy.ext.associationproxy import association_proxy @@ -162,7 +161,7 @@ class Playdates(Base): __tablename__ = "playdates" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - lastplayed: Mapped[datetime] = mapped_column(index=True) + lastplayed: Mapped[dt.datetime] = mapped_column(index=True) track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id")) track: Mapped["Tracks"] = relationship("Tracks", back_populates="playdates") @@ -175,13 +174,13 @@ class Playdates(Base): def __init__(self, session: scoped_session, track_id: int) -> None: """Record that track was played""" - self.lastplayed = datetime.now() + self.lastplayed = dt.datetime.now() self.track_id = track_id session.add(self) session.commit() @staticmethod - def last_played(session: scoped_session, track_id: int) -> datetime: + def last_played(session: scoped_session, track_id: int) -> dt.datetime: """Return datetime track last played or None""" last_played = session.execute( @@ -194,10 +193,12 @@ class Playdates(Base): if last_played: return last_played[0] else: - return Config.EPOCH + # Should never be reached as we create record with a + # last_played value + return Config.EPOCH # pragma: no cover @staticmethod - def played_after(session: scoped_session, since: datetime) -> Sequence["Playdates"]: + def played_after(session: scoped_session, since: dt.datetime) -> Sequence["Playdates"]: """Return a list of Playdates objects since passed time""" return session.scalars( @@ -216,7 +217,7 @@ class Playlists(Base): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(32), unique=True) - last_used: Mapped[Optional[datetime]] = mapped_column(DateTime, default=None) + last_used: Mapped[Optional[dt.datetime]] = mapped_column(DateTime, default=None) tab: Mapped[Optional[int]] = mapped_column(default=None) open: Mapped[bool] = mapped_column(default=False) is_template: Mapped[bool] = mapped_column(default=False) @@ -328,7 +329,7 @@ class Playlists(Base): """Mark playlist as loaded and used now""" self.open = True - self.last_used = datetime.now() + self.last_used = dt.datetime.now() @staticmethod def name_is_available(session: scoped_session, name: str) -> bool: @@ -586,8 +587,6 @@ class PlaylistRows(Base): cls, session: scoped_session, playlist_id: int, - from_row: Optional[int] = None, - to_row: Optional[int] = None, ) -> Sequence["PlaylistRows"]: """ For passed playlist, return a list of rows that @@ -597,11 +596,6 @@ class PlaylistRows(Base): query = select(cls).where( cls.playlist_id == playlist_id, cls.track_id.is_not(None) ) - if from_row is not None: - query = query.where(cls.plr_rownum >= from_row) - if to_row is not None: - query = query.where(cls.plr_rownum <= to_row) - plrs = session.scalars((query).order_by(cls.plr_rownum)).all() return plrs @@ -682,7 +676,7 @@ class Settings(Base): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(64), unique=True) - f_datetime: Mapped[Optional[datetime]] = mapped_column(default=None) + f_datetime: Mapped[Optional[dt.datetime]] = mapped_column(default=None) f_int: Mapped[Optional[int]] = mapped_column(default=None) f_string: Mapped[Optional[str]] = mapped_column(String(128), default=None) diff --git a/app/musicmuster.py b/app/musicmuster.py index 9ab262a..b20cedf 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -from datetime import datetime, timedelta +import datetime as dt from time import sleep from typing import ( cast, @@ -208,14 +208,12 @@ class Window(QMainWindow, Ui_MainWindow): self.music: music.Music = music.Music() self.playing: bool = False - self.selected_plrs: Optional[List[PlaylistRows]] = None - self.set_main_window_size() self.lblSumPlaytime = QLabel("") self.statusbar.addPermanentWidget(self.lblSumPlaytime) self.txtSearch = QLineEdit() - self.statusbar.addWidget(self.txtSearch) self.txtSearch.setHidden(True) + self.statusbar.addWidget(self.txtSearch) self.hide_played_tracks = False mixer.init() self.widgetFadeVolume.hideAxis("bottom") @@ -755,7 +753,7 @@ class Window(QMainWindow, Ui_MainWindow): if track_sequence.now.track_id is None or track_sequence.now.start_time is None: return 0 - now = datetime.now() + now = dt.datetime.now() track_start = track_sequence.now.start_time elapsed_seconds = (now - track_start).total_seconds() return int(elapsed_seconds * 1000) @@ -902,7 +900,7 @@ class Window(QMainWindow, Ui_MainWindow): if playlist: _ = self.create_playlist_tab(playlist) playlist_ids.append(playlist.id) - log.info(f"load_last_playlists() loaded {playlist=}") + log.debug(f"load_last_playlists() loaded {playlist=}") # Set active tab record = Settings.get_int_settings(session, "active_tab") if record.f_int is not None and record.f_int >= 0: @@ -1236,7 +1234,7 @@ class Window(QMainWindow, Ui_MainWindow): and track_sequence.now.resume_marker ): elapsed_ms = track_sequence.now.duration * track_sequence.now.resume_marker - track_sequence.now.start_time -= timedelta(milliseconds=elapsed_ms) + track_sequence.now.start_time -= dt.timedelta(milliseconds=elapsed_ms) def save_as_template(self) -> None: """Save current playlist as template""" @@ -1497,7 +1495,7 @@ class Window(QMainWindow, Ui_MainWindow): and track_sequence.now.start_time ): play_time = ( - datetime.now() - track_sequence.now.start_time + dt.datetime.now() - track_sequence.now.start_time ).total_seconds() * 1000 track_sequence.now.fade_graph.tick(play_time) @@ -1506,7 +1504,7 @@ class Window(QMainWindow, Ui_MainWindow): Called every 500ms """ - self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT)) + self.lblTOD.setText(dt.datetime.now().strftime(Config.TOD_TIME_FORMAT)) # Update carts # self.cart_tick() @@ -1534,8 +1532,8 @@ class Window(QMainWindow, Ui_MainWindow): and track_sequence.now.start_time and ( self.music.player.is_playing() - or (datetime.now() - track_sequence.now.start_time) - < timedelta(microseconds=Config.PLAY_SETTLE) + or (dt.datetime.now() - track_sequence.now.start_time) + < dt.timedelta(microseconds=Config.PLAY_SETTLE) ) ): playtime = self.get_playtime() diff --git a/app/playlistmodel.py b/app/playlistmodel.py index a91438d..6408176 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -2,11 +2,9 @@ from __future__ import annotations import obsws_python as obs # type: ignore import re -from dataclasses import dataclass -from datetime import datetime, timedelta +import datetime as dt from enum import auto, Enum from operator import attrgetter -from pprint import pprint from random import shuffle from typing import List, Optional @@ -64,13 +62,13 @@ class PlaylistRowData: self.artist: str = "" self.bitrate = 0 self.duration: int = 0 - self.lastplayed: datetime = Config.EPOCH + self.lastplayed: dt.datetime = Config.EPOCH self.path = "" self.played = False self.start_gap: Optional[int] = None self.title: str = "" - self.start_time: Optional[datetime] = None - self.end_time: Optional[datetime] = None + self.start_time: Optional[dt.datetime] = None + self.end_time: Optional[dt.datetime] = None self.plrid: int = plr.id self.plr_rownum: int = plr.plr_rownum @@ -685,7 +683,7 @@ class PlaylistModel(QAbstractTableModel): < prd.plr_rownum ) ): - section_end_time = track_sequence.now.end_time + timedelta( + section_end_time = track_sequence.now.end_time + dt.timedelta( milliseconds=duration ) end_time_str = ( @@ -1341,7 +1339,7 @@ class PlaylistModel(QAbstractTableModel): log.info("update_track_times()") - next_start_time: Optional[datetime] = None + next_start_time: Optional[dt.datetime] = None update_rows: List[int] = [] for row_number in range(len(self.playlist_rows)): @@ -1362,7 +1360,7 @@ class PlaylistModel(QAbstractTableModel): and track_sequence.now.end_time ): prd.start_time = track_sequence.now.end_time - prd.end_time = prd.start_time + timedelta(milliseconds=prd.duration) + prd.end_time = prd.start_time + dt.timedelta(milliseconds=prd.duration) next_start_time = prd.end_time update_rows.append(row_number) continue @@ -1407,7 +1405,7 @@ class PlaylistModel(QAbstractTableModel): update_rows.append(row_number) # Calculate next start time - next_start_time += timedelta(milliseconds=prd.duration) + next_start_time += dt.timedelta(milliseconds=prd.duration) # Update end time of this row if it's incorrect if prd.end_time != next_start_time: @@ -1483,9 +1481,9 @@ class PlaylistProxyModel(QSortFilterProxyModel): == self.source_model.playlist_id ): if track_sequence.now.start_time: - if datetime.now() > ( + if dt.datetime.now() > ( track_sequence.now.start_time - + timedelta( + + dt.timedelta( milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET ) ): diff --git a/archive/audplayer.py b/archive/audplayer.py index 338d608..d9442ef 100755 --- a/archive/audplayer.py +++ b/archive/audplayer.py @@ -1,11 +1,11 @@ #!/usr/bin/python3 -from datetime import datetime, timedelta +import datetime as dt from threading import Timer from pydub import AudioSegment from time import sleep -from timeloop import Timeloop -import vlc +from timeloop import Timeloop # type: ignore +import vlc # type: ignore class RepeatedTimer(object): @@ -124,7 +124,7 @@ def update_progress(player, talk_at, silent_at): remaining_time = total_time - elapsed_time talk_time = remaining_time - (total_time - talk_at) silent_time = remaining_time - (total_time - silent_at) - end_time = (datetime.now() + timedelta( + end_time = (dt.datetime.now() + timedelta( milliseconds=remaining_time)).strftime("%H:%M:%S") print( f"\t{ms_to_mmss(elapsed_time)}/" diff --git a/poetry.lock b/poetry.lock index ef250f6..6a5cd67 100644 --- a/poetry.lock +++ b/poetry.lock @@ -292,6 +292,74 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] development = ["black", "flake8", "mypy", "pytest", "types-colorama"] +[[package]] +name = "coverage" +version = "7.4.4" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, + {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, + {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, + {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, + {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "decorator" version = "5.1.1" @@ -1524,6 +1592,25 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "pytest-qt" version = "4.4.0" @@ -2186,4 +2273,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "9fc13f4695a3be773cbbdcc175495650e638f31d786f6e35724b57f70bcb5f78" +content-hash = "500cefc31e30cba9ae917cc51b7407961d69825d1fcae53515ed1fa12f4ab171" diff --git a/pyproject.toml b/pyproject.toml index e5257f5..d8840a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ black = "^24.2.0" flakehell = "^0.9.0" mypy = "^1.7.0" pdbp = "^1.5.0" +pytest-cov = "^5.0.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/test_helpers.py b/tests/test_helpers.py similarity index 87% rename from test_helpers.py rename to tests/test_helpers.py index a1b9a6d..f2ddecd 100644 --- a/test_helpers.py +++ b/tests/test_helpers.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +import datetime as dt from helpers import ( fade_point, get_audio_segment, @@ -43,12 +43,12 @@ def test_get_tags(): def test_get_relative_date(): assert get_relative_date(None) == "Never" - today_at_10 = datetime.now().replace(hour=10, minute=0) - today_at_11 = datetime.now().replace(hour=11, minute=0) + today_at_10 = dt.datetime.now().replace(hour=10, minute=0) + today_at_11 = dt.datetime.now().replace(hour=11, minute=0) assert get_relative_date(today_at_10, today_at_11) == "Today 10:00" - eight_days_ago = today_at_10 - timedelta(days=8) + eight_days_ago = today_at_10 - dt.timedelta(days=8) assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago" - sixteen_days_ago = today_at_10 - timedelta(days=16) + sixteen_days_ago = today_at_10 - dt.timedelta(days=16) assert get_relative_date(sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago" diff --git a/tests/test_misc.py b/tests/test_misc.py new file mode 100644 index 0000000..68b5d88 --- /dev/null +++ b/tests/test_misc.py @@ -0,0 +1,25 @@ +import pytest +from models import NoteColours, Settings + + +def test_log_exception(): + """Test deliberate exception""" + + with pytest.raises(Exception): + 1 / 0 + + +def test_create_settings(session): + SETTING_NAME = "wombat" + NO_SUCH_SETTING = "abc" + VALUE = 3 + + setting = Settings(session, SETTING_NAME) + setting.update(session, dict(f_int=VALUE)) + print(setting) + _ = Settings.all_as_dict(session) + test = Settings.get_int_settings(session, SETTING_NAME) + assert test.name == SETTING_NAME + assert test.f_int == VALUE + test_new = Settings.get_int_settings(session, NO_SUCH_SETTING) + assert test_new.name == NO_SUCH_SETTING diff --git a/test_models.py b/tests/test_models.py similarity index 52% rename from test_models.py rename to tests/test_models.py index bc02c24..8306ff2 100644 --- a/test_models.py +++ b/tests/test_models.py @@ -1,11 +1,11 @@ -import os.path - -import helpers +import datetime as dt from app.models import ( + Carts, NoteColours, Playdates, Playlists, + PlaylistRows, Tracks, ) @@ -60,68 +60,44 @@ def test_playdates_add_playdate(session, track1): playdate = Playdates(session, track1.id) assert playdate + print(playdate) last_played = Playdates.last_played(session, track1.id) assert abs((playdate.lastplayed - last_played).total_seconds()) < 2 +def test_playdates_played_after(session, track1): + playdate = Playdates(session, track1.id) + yesterday = dt.datetime.now() - dt.timedelta(days=1) + played = Playdates.played_after(session, yesterday) + + assert len(played) == 1 + assert played[0] == playdate + + def test_playlist_create(session): + TEMPLATE_NAME = "my template" + playlist = Playlists(session, "my playlist") assert playlist + print(playlist) + # test clear tabs + Playlists.clear_tabs(session, [playlist.id]) -# def test_playlist_add_track(session, track): -# # We need a playlist -# playlist = Playlists(session, "my playlist") + # create template + Playlists.save_as_template(session, playlist.id, TEMPLATE_NAME) -# row = 17 + # test create template + _ = Playlists.create_playlist_from_template(session, playlist, "my new name") -# playlist.add_track(session, track.id, row) - -# assert len(playlist.tracks) == 1 -# playlist_track = playlist.tracks[row] -# assert playlist_track.path == track_path - - -# def test_playlist_tracks(session): -# # We need a playlist -# playlist = Playlists(session, "my playlist") - -# # We need two tracks -# track1_path = "/a/b/c" -# track1_row = 17 -# track1 = Tracks(session, track1_path) - -# track2_path = "/x/y/z" -# track2_row = 29 -# track2 = Tracks(session, track2_path) - -# playlist.add_track(session, track1.id, track1_row) -# playlist.add_track(session, track2.id, track2_row) - -# tracks = playlist.tracks -# assert tracks[track1_row] == track1 -# assert tracks[track2_row] == track2 - - -# def test_playlist_notes(session): -# # We need a playlist -# playlist = Playlists(session, "my playlist") - -# # We need two notes -# note1_text = "note1 text" -# note1_row = 11 -# _ = Notes(session, playlist.id, note1_row, note1_text) - -# note2_text = "note2 text" -# note2_row = 19 -# _ = Notes(session, playlist.id, note2_row, note2_text) - -# notes = playlist.notes -# assert note1_text in [n.note for n in notes] -# assert note1_row in [n.row for n in notes] -# assert note2_text in [n.note for n in notes] -# assert note2_row in [n.row for n in notes] + # get all templates + all_templates = Playlists.get_all_templates(session) + assert len(all_templates) == 1 + # Save as template creates new playlist + assert all_templates[0] != playlist + # test delete playlist + playlist.delete(session) def test_playlist_open_and_close(session): @@ -184,3 +160,91 @@ def test_tracks_search_titles(session, track1): track1_title = "I'm So Afraid" assert len(Tracks.search_titles(session, track1_title)) == 1 + +def test_repr(session): + """Just check for error retrieving reprs""" + + nc = NoteColours(session, substring="x", colour="x") + print(nc) + + +def test_get_colour(session): + """Test for errors in execution""" + + GOOD_STRING = "cantelope" + BAD_STRING = "ericTheBee" + SUBSTR = "ant" + COLOUR = "blue" + + nc1 = NoteColours(session, substring=SUBSTR, colour=COLOUR, is_casesensitive=True) + + _ = nc1.get_colour(session, "") + colour = nc1.get_colour(session, GOOD_STRING) + assert colour == COLOUR + + colour = nc1.get_colour(session, BAD_STRING) + assert colour is None + + nc2 = NoteColours(session, substring=".*" + SUBSTR, colour=COLOUR, is_regex=True) + colour = nc2.get_colour(session, GOOD_STRING) + assert colour == COLOUR + + colour = nc2.get_colour(session, BAD_STRING) + assert colour is None + + nc3 = NoteColours( + session, substring=".*" + SUBSTR, colour=COLOUR, is_regex=True, is_casesensitive=True + ) + + colour = nc3.get_colour(session, GOOD_STRING) + assert colour == COLOUR + + colour = nc3.get_colour(session, BAD_STRING) + assert colour is None + + +def test_create_cart(session): + cart = Carts(session, 1, "name") + assert cart + print(cart) + + +def test_name_available(session): + PLAYLIST_NAME = "a name" + RENAME = "new name" + + if Playlists.name_is_available(session, PLAYLIST_NAME): + playlist = Playlists(session, PLAYLIST_NAME) + assert playlist + + assert Playlists.name_is_available(session, PLAYLIST_NAME) is False + + playlist.rename(session, RENAME) + + +def test_create_playlist_row(session): + PLAYLIST_NAME = "a name" + + if Playlists.name_is_available(session, PLAYLIST_NAME): + playlist = Playlists(session, PLAYLIST_NAME) + + plr = PlaylistRows(session, playlist.id, 1) + assert plr + print(plr) + plr.append_note("a note") + plr.append_note("another note") + + +def test_delete_plr(session): + PLAYLIST_NAME = "a name" + + if Playlists.name_is_available(session, PLAYLIST_NAME): + playlist = Playlists(session, PLAYLIST_NAME) + + plr = PlaylistRows(session, playlist.id, 1) + assert plr + PlaylistRows.delete_higher_rows(session, plr.playlist_id, 10) + + assert PlaylistRows.get_track_plr(session, 12, plr.playlist_id) is None + + diff --git a/test_playlistmodel.py b/tests/test_playlistmodel.py similarity index 96% rename from test_playlistmodel.py rename to tests/test_playlistmodel.py index c1b09cc..4a2ec51 100644 --- a/test_playlistmodel.py +++ b/tests/test_playlistmodel.py @@ -1,4 +1,3 @@ -from pprint import pprint from typing import Optional from app.models import ( @@ -228,6 +227,20 @@ def test_insert_header_row_middle(monkeypatch, session): ) +def test_add_track_to_header(monkeypatch, session): + monkeypatch.setattr(playlistmodel, "Session", session) + note_text = "test text" + initial_row_count = 11 + insert_row = 6 + + model = create_model_with_playlist_rows(session, initial_row_count) + model.insert_row(proposed_row_number=insert_row, note=note_text) + assert model.rowCount() == initial_row_count + 1 + + prd = model.playlist_rows[1] + model.add_track_to_header(insert_row, prd.track_id) + + def test_create_model_with_tracks(monkeypatch, session): monkeypatch.setattr(playlistmodel, "Session", session) model = create_model_with_tracks(session) @@ -303,7 +316,7 @@ def test_move_one_row_between_playlists_to_end(monkeypatch, session): model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination") - model_src.move_rows_between_playlists(from_rows, to_row, model_dst) + model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) model_dst.refresh_data(session) assert len(model_src.playlist_rows) == create_rowcount - len(from_rows) @@ -323,7 +336,7 @@ def test_move_one_row_between_playlists_to_middle(monkeypatch, session): model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination") - model_src.move_rows_between_playlists(from_rows, to_row, model_dst) + model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) model_dst.refresh_data(session) # Check the rows of the destination model @@ -347,7 +360,7 @@ def test_move_multiple_rows_between_playlists_to_end(monkeypatch, session): model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination") - model_src.move_rows_between_playlists(from_rows, to_row, model_dst) + model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) model_dst.refresh_data(session) # Check the rows of the destination model From 6fd541060e58cafc43347b9d54422864376c4dda Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Tue, 2 Apr 2024 21:42:22 +0100 Subject: [PATCH 2/7] Migrate to Alchemical --- app/classes.py | 9 ++- app/dbconfig.py | 38 --------- app/dbtables.py | 187 ++++++++++++++++++++++++++++++++++++++++++ app/dialogs.py | 9 ++- app/models.py | 189 ++++++------------------------------------- app/musicmuster.py | 79 +++++++++--------- app/playlistmodel.py | 48 ++++++----- app/playlists.py | 21 ++--- poetry.lock | 20 ++++- pyproject.toml | 1 + 10 files changed, 321 insertions(+), 280 deletions(-) delete mode 100644 app/dbconfig.py create mode 100644 app/dbtables.py diff --git a/app/classes.py b/app/classes.py index 35edb26..d3be8fe 100644 --- a/app/classes.py +++ b/app/classes.py @@ -1,13 +1,18 @@ +# Standard library imports from dataclasses import dataclass -import datetime as dt from typing import Optional +import datetime as dt +# PyQt imports from PyQt6.QtCore import pyqtSignal, QObject, QThread + +# Third party imports import numpy as np import pyqtgraph as pg # type: ignore +from sqlalchemy.orm import scoped_session +# App imports from config import Config -from dbconfig import scoped_session from models import PlaylistRows import helpers diff --git a/app/dbconfig.py b/app/dbconfig.py deleted file mode 100644 index 30dc603..0000000 --- a/app/dbconfig.py +++ /dev/null @@ -1,38 +0,0 @@ -import inspect -import os -from config import Config -from contextlib import contextmanager -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker, scoped_session -from typing import Generator - -from log import log - -MYSQL_CONNECT = os.environ.get("MM_DB") -if MYSQL_CONNECT is None: - raise ValueError("MYSQL_CONNECT is undefined") -else: - dbname = MYSQL_CONNECT.split("/")[-1] - log.debug(f"Database: {dbname}") - -engine = create_engine( - MYSQL_CONNECT, - echo=Config.DISPLAY_SQL, - pool_pre_ping=True, - future=True, - connect_args={"charset": "utf8mb4"}, -) - - -@contextmanager -def Session() -> Generator[scoped_session, None, None]: - frame = inspect.stack()[2] - file = frame.filename - function = frame.function - lineno = frame.lineno - Session = scoped_session(sessionmaker(bind=engine)) - log.debug(f"Session acquired: {file}:{function}:{lineno} " f"[{hex(id(Session))}]") - yield Session - log.debug(f" Session released [{hex(id(Session))}]") - Session.commit() - Session.close() diff --git a/app/dbtables.py b/app/dbtables.py new file mode 100644 index 0000000..93b8e24 --- /dev/null +++ b/app/dbtables.py @@ -0,0 +1,187 @@ +# Standard library imports +from typing import List, Optional +import datetime as dt +import os + +# PyQt imports + +# Third party imports +from alchemical import Alchemical, Model # type: ignore +from sqlalchemy import ( + Boolean, + DateTime, + ForeignKey, + String, +) +from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) + +# App imports + + +# Database classes +# Note: initialisation of the 'db' variable is at the foot of this +# module. +class CartsTable(Model): + __tablename__ = "carts" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + cart_number: Mapped[int] = mapped_column(unique=True) + name: Mapped[str] = mapped_column(String(256), index=True) + duration: Mapped[Optional[int]] = mapped_column(index=True) + path: Mapped[Optional[str]] = mapped_column(String(2048), index=False) + enabled: Mapped[Optional[bool]] = mapped_column(default=False) + + def __repr__(self) -> str: + return ( + f"" + ) + + +class NoteColoursTable(Model): + __tablename__ = "notecolours" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + substring: Mapped[str] = mapped_column(String(256), index=False) + colour: Mapped[str] = mapped_column(String(21), index=False) + enabled: Mapped[bool] = mapped_column(default=True, index=True) + is_regex: Mapped[bool] = mapped_column(default=False, index=False) + is_casesensitive: Mapped[bool] = mapped_column(default=False, index=False) + order: Mapped[Optional[int]] = mapped_column(index=True) + + def __repr__(self) -> str: + return ( + f"" + ) + + +class PlaydatesTable(Model): + __tablename__ = "playdates" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + lastplayed: Mapped[dt.datetime] = mapped_column(index=True) + track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id")) + track: Mapped["TracksTable"] = relationship("Tracks", back_populates="playdates") + + def __repr__(self) -> str: + return ( + f"" + ) + + +class PlaylistsTable(Model): + """ + Manage playlists + """ + + __tablename__ = "playlists" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(32), unique=True) + last_used: Mapped[Optional[dt.datetime]] = mapped_column(DateTime, default=None) + tab: Mapped[Optional[int]] = mapped_column(default=None) + open: Mapped[bool] = mapped_column(default=False) + is_template: Mapped[bool] = mapped_column(default=False) + deleted: Mapped[bool] = mapped_column(default=False) + rows: Mapped[List["PlaylistRowsTable"]] = relationship( + "PlaylistRows", + back_populates="playlist", + cascade="all, delete-orphan", + order_by="PlaylistRows.plr_rownum", + ) + + def __repr__(self) -> str: + return ( + f"" + ) + + +class PlaylistRowsTable(Model): + __tablename__ = "playlist_rows" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + plr_rownum: Mapped[int] + note: Mapped[str] = mapped_column( + String(2048), index=False, default="", nullable=False + ) + playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id")) + playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows") + track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id")) + track: Mapped["TracksTable"] = relationship( + "Tracks", + back_populates="playlistrows", + ) + played: Mapped[bool] = mapped_column( + Boolean, nullable=False, index=False, default=False + ) + + def __repr__(self) -> str: + return ( + f"" + ) + + +class SettingsTable(Model): + """Manage settings""" + + __tablename__ = "settings" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(64), unique=True) + f_datetime: Mapped[Optional[dt.datetime]] = mapped_column(default=None) + f_int: Mapped[Optional[int]] = mapped_column(default=None) + f_string: Mapped[Optional[str]] = mapped_column(String(128), default=None) + + def __repr__(self) -> str: + return ( + f"" + ) + + +class TracksTable(Model): + __tablename__ = "tracks" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + title: Mapped[str] = mapped_column(String(256), index=True) + artist: Mapped[str] = mapped_column(String(256), index=True) + bitrate: Mapped[Optional[int]] = mapped_column(default=None) + duration: Mapped[int] = mapped_column(index=True) + fade_at: Mapped[int] = mapped_column(index=False) + mtime: Mapped[float] = mapped_column(index=True) + path: Mapped[str] = mapped_column(String(2048), index=False, unique=True) + silence_at: Mapped[int] = mapped_column(index=False) + start_gap: Mapped[int] = mapped_column(index=False) + playlistrows: Mapped[List[PlaylistRowsTable]] = relationship( + "PlaylistRows", back_populates="track" + ) + playlists = association_proxy("playlistrows", "playlist") + playdates: Mapped[List[PlaydatesTable]] = relationship( + "Playdates", + back_populates="track", + lazy="joined", + ) + + def __repr__(self) -> str: + return ( + f"" + ) + + +MYSQL_CONNECT = os.environ.get("MM_DB") +if MYSQL_CONNECT is None: + raise ValueError("MYSQL_CONNECT is undefined") +else: + dbname = MYSQL_CONNECT.split("/")[-1] +db = Alchemical(MYSQL_CONNECT) diff --git a/app/dialogs.py b/app/dialogs.py index 31e015a..ab5465e 100644 --- a/app/dialogs.py +++ b/app/dialogs.py @@ -1,10 +1,17 @@ +# Standard library imports + +# PyQt imports + +# Third party imports + +# App imports from typing import Optional from PyQt6.QtCore import QEvent, Qt from PyQt6.QtWidgets import QDialog, QListWidgetItem from classes import MusicMusterSignals -from dbconfig import scoped_session +from sqlalchemy.orm import scoped_session from helpers import ( ask_yes_no, get_relative_date, diff --git a/app/models.py b/app/models.py index 56d186d..3b15c20 100644 --- a/app/models.py +++ b/app/models.py @@ -1,63 +1,37 @@ -#!/usr/bin/python3 - +# Standard library imports +from typing import List, Optional, Sequence +import datetime as dt import re -from config import Config -from dbconfig import scoped_session - -import datetime as dt -from typing import List, Optional, Sequence - -from sqlalchemy.ext.associationproxy import association_proxy +# PyQt imports +# Third party imports from sqlalchemy import ( bindparam, - Boolean, - DateTime, delete, - ForeignKey, func, select, - String, update, ) - -from sqlalchemy.orm import ( - DeclarativeBase, - joinedload, - Mapped, - mapped_column, - relationship, -) -from sqlalchemy.orm.exc import ( - NoResultFound, -) from sqlalchemy.exc import ( IntegrityError, ) +from sqlalchemy.orm import ( + joinedload, + scoped_session, +) +from sqlalchemy.orm.exc import ( + NoResultFound, +) + +# App imports +import dbtables +from config import Config from log import log -class Base(DeclarativeBase): - pass - - # Database classes -class Carts(Base): - __tablename__ = "carts" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - cart_number: Mapped[int] = mapped_column(unique=True) - name: Mapped[str] = mapped_column(String(256), index=True) - duration: Mapped[Optional[int]] = mapped_column(index=True) - path: Mapped[Optional[str]] = mapped_column(String(2048), index=False) - enabled: Mapped[Optional[bool]] = mapped_column(default=False) - - def __repr__(self) -> str: - return ( - f"" - ) +class Carts(dbtables.CartsTable): def __init__( self, @@ -80,22 +54,7 @@ class Carts(Base): session.commit() -class NoteColours(Base): - __tablename__ = "notecolours" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - substring: Mapped[str] = mapped_column(String(256), index=False) - colour: Mapped[str] = mapped_column(String(21), index=False) - enabled: Mapped[bool] = mapped_column(default=True, index=True) - is_regex: Mapped[bool] = mapped_column(default=False, index=False) - is_casesensitive: Mapped[bool] = mapped_column(default=False, index=False) - order: Mapped[Optional[int]] = mapped_column(index=True) - - def __repr__(self) -> str: - return ( - f"" - ) +class NoteColours(dbtables.NoteColoursTable): def __init__( self, @@ -157,19 +116,7 @@ class NoteColours(Base): return None -class Playdates(Base): - __tablename__ = "playdates" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - lastplayed: Mapped[dt.datetime] = mapped_column(index=True) - track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id")) - track: Mapped["Tracks"] = relationship("Tracks", back_populates="playdates") - - def __repr__(self) -> str: - return ( - f"" - ) +class Playdates(dbtables.PlaydatesTable): def __init__(self, session: scoped_session, track_id: int) -> None: """Record that track was played""" @@ -208,32 +155,7 @@ class Playdates(Base): ).all() -class Playlists(Base): - """ - Manage playlists - """ - - __tablename__ = "playlists" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - name: Mapped[str] = mapped_column(String(32), unique=True) - last_used: Mapped[Optional[dt.datetime]] = mapped_column(DateTime, default=None) - tab: Mapped[Optional[int]] = mapped_column(default=None) - open: Mapped[bool] = mapped_column(default=False) - is_template: Mapped[bool] = mapped_column(default=False) - deleted: Mapped[bool] = mapped_column(default=False) - rows: Mapped[List["PlaylistRows"]] = relationship( - "PlaylistRows", - back_populates="playlist", - cascade="all, delete-orphan", - order_by="PlaylistRows.plr_rownum", - ) - - def __repr__(self) -> str: - return ( - f"" - ) +class Playlists(dbtables.PlaylistsTable): def __init__(self, session: scoped_session, name: str): self.name = name @@ -366,31 +288,7 @@ class Playlists(Base): PlaylistRows.copy_playlist(session, playlist_id, template.id) -class PlaylistRows(Base): - __tablename__ = "playlist_rows" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - plr_rownum: Mapped[int] - note: Mapped[str] = mapped_column( - String(2048), index=False, default="", nullable=False - ) - playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id")) - playlist: Mapped[Playlists] = relationship(back_populates="rows") - track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id")) - track: Mapped["Tracks"] = relationship( - "Tracks", - back_populates="playlistrows", - ) - played: Mapped[bool] = mapped_column( - Boolean, nullable=False, index=False, default=False - ) - - def __repr__(self) -> str: - return ( - f"" - ) +class PlaylistRows(dbtables.PlaylistRowsTable): def __init__( self, @@ -669,27 +567,7 @@ class PlaylistRows(Base): session.connection().execute(stmt, sqla_map) -class Settings(Base): - """Manage settings""" - - __tablename__ = "settings" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - name: Mapped[str] = mapped_column(String(64), unique=True) - f_datetime: Mapped[Optional[dt.datetime]] = mapped_column(default=None) - f_int: Mapped[Optional[int]] = mapped_column(default=None) - f_string: Mapped[Optional[str]] = mapped_column(String(128), default=None) - - def __repr__(self) -> str: - return ( - f"" - ) - - def __init__(self, session: scoped_session, name: str): - self.name = name - session.add(self) - session.flush() +class Settings(dbtables.SettingsTable): @classmethod def all_as_dict(cls, session): @@ -722,28 +600,7 @@ class Settings(Base): session.flush() -class Tracks(Base): - __tablename__ = "tracks" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - title: Mapped[str] = mapped_column(String(256), index=True) - artist: Mapped[str] = mapped_column(String(256), index=True) - bitrate: Mapped[Optional[int]] = mapped_column(default=None) - duration: Mapped[int] = mapped_column(index=True) - fade_at: Mapped[int] = mapped_column(index=False) - mtime: Mapped[float] = mapped_column(index=True) - path: Mapped[str] = mapped_column(String(2048), index=False, unique=True) - silence_at: Mapped[int] = mapped_column(index=False) - start_gap: Mapped[int] = mapped_column(index=False) - playlistrows: Mapped[List[PlaylistRows]] = relationship( - "PlaylistRows", back_populates="track" - ) - playlists = association_proxy("playlistrows", "playlist") - playdates: Mapped[List[Playdates]] = relationship( - "Playdates", - back_populates="track", - lazy="joined", - ) +class Tracks(dbtables.TracksTable): def __repr__(self) -> str: return ( diff --git a/app/musicmuster.py b/app/musicmuster.py index b20cedf..1c56bba 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -1,22 +1,17 @@ #!/usr/bin/env python3 -import datetime as dt -from time import sleep -from typing import ( - cast, - List, - Optional, -) +# Standard library imports from os.path import basename - +from time import sleep +from typing import cast, List, Optional import argparse +import datetime as dt import os import subprocess import sys import threading -import pipeclient -from pygame import mixer +# PyQt imports from PyQt6.QtCore import ( pyqtSignal, QDate, @@ -49,8 +44,14 @@ from PyQt6.QtWidgets import ( QProgressBar, QPushButton, ) + +# Third party imports +from pygame import mixer +import pipeclient +from sqlalchemy.orm import scoped_session import stackprinter # type: ignore +# App imports from classes import ( track_sequence, FadeCurve, @@ -58,23 +59,19 @@ from classes import ( PlaylistTrack, ) from config import Config -from dbconfig import ( - engine, - scoped_session, - Session, -) +from dbtables import db from dialogs import TrackSelectDialog from log import log -from models import Base, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks +from models import Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks from playlistmodel import PlaylistModel, PlaylistProxyModel from playlists import PlaylistTab +from ui import icons_rc # noqa F401 from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore from ui.downloadcsv_ui import Ui_DateSelect # type: ignore from ui.main_window_ui import Ui_MainWindow # type: ignore from utilities import check_db, update_bitrates import helpers -from ui import icons_rc # noqa F401 import music @@ -168,7 +165,7 @@ class ImportTrack(QObject): Create track objects from passed files and add to visible playlist """ - with Session() as session: + with db.Session() as session: for fname in self.filenames: self.signals.status_message_signal.emit( f"Importing {basename(fname)}", 5000 @@ -255,7 +252,7 @@ class Window(QMainWindow, Ui_MainWindow): except subprocess.CalledProcessError as exc_info: git_tag = str(exc_info.output) - with Session() as session: + with db.Session() as session: if session.bind: dbname = session.bind.engine.url.database @@ -317,7 +314,7 @@ class Window(QMainWindow, Ui_MainWindow): def cart_edit(self, btn: CartButton, event: QEvent): """Handle context menu for cart button""" - with Session() as session: + with db.Session() as session: cart = session.query(Carts).get(btn.cart_id) if cart is None: log.error("cart_edit: cart not found") @@ -349,7 +346,7 @@ class Window(QMainWindow, Ui_MainWindow): def carts_init(self) -> None: """Initialse carts data structures""" - with Session() as session: + with db.Session() as session: # Number carts from 1 for humanity for cart_number in range(1, Config.CARTS_COUNT + 1): cart = session.query(Carts).get(cart_number) @@ -426,7 +423,7 @@ class Window(QMainWindow, Ui_MainWindow): self, "Track playing", "Can't close application while track is playing" ) else: - with Session() as session: + with db.Session() as session: settings = Settings.all_as_dict(session) record = settings["mainwindow_height"] if record.f_int != self.height(): @@ -495,7 +492,7 @@ class Window(QMainWindow, Ui_MainWindow): return False # Record playlist as closed and update remaining playlist tabs - with Session() as session: + with db.Session() as session: playlist = session.get(Playlists, closing_tab_playlist_id) if playlist: playlist.close() @@ -588,7 +585,7 @@ class Window(QMainWindow, Ui_MainWindow): def create_and_show_playlist(self) -> None: """Create new playlist and display it""" - with Session() as session: + with db.Session() as session: playlist = self.create_playlist(session) if playlist: self.create_playlist_tab(playlist) @@ -636,7 +633,7 @@ class Window(QMainWindow, Ui_MainWindow): Delete current playlist """ - with Session() as session: + with db.Session() as session: playlist_id = self.active_tab().playlist_id playlist = session.get(Playlists, playlist_id) if playlist: @@ -670,7 +667,7 @@ class Window(QMainWindow, Ui_MainWindow): path += ".csv" with open(path, "w") as f: - with Session() as session: + with db.Session() as session: for playdate in Playdates.played_after(session, start_dt): f.write(f"{playdate.track.artist},{playdate.track.title}\n") @@ -702,7 +699,7 @@ class Window(QMainWindow, Ui_MainWindow): playlist_id = self.active_tab().playlist_id - with Session() as session: + with db.Session() as session: # Get output filename playlist = session.get(Playlists, playlist_id) if not playlist: @@ -784,7 +781,7 @@ class Window(QMainWindow, Ui_MainWindow): if not dlg.exec(): return - with Session() as session: + with db.Session() as session: new_tracks = [] for fname in dlg.selectedFiles(): txt = "" @@ -883,7 +880,7 @@ class Window(QMainWindow, Ui_MainWindow): self.active_tab().source_model_selected_row_number() or self.active_proxy_model().rowCount() ) - with Session() as session: + with db.Session() as session: dlg = TrackSelectDialog( session=session, new_row_number=new_row_number, @@ -895,7 +892,7 @@ class Window(QMainWindow, Ui_MainWindow): """Load the playlists that were open when the last session closed""" playlist_ids = [] - with Session() as session: + with db.Session() as session: for playlist in Playlists.get_open(session): if playlist: _ = self.create_playlist_tab(playlist) @@ -952,7 +949,7 @@ class Window(QMainWindow, Ui_MainWindow): visible_tab = self.active_tab() source_playlist_id = visible_tab.playlist_id - with Session() as session: + with db.Session() as session: for playlist in Playlists.get_all(session): if playlist.id == source_playlist_id: continue @@ -1005,7 +1002,7 @@ class Window(QMainWindow, Ui_MainWindow): def new_from_template(self) -> None: """Create new playlist from template""" - with Session() as session: + with db.Session() as session: templates = Playlists.get_all_templates(session) dlg = SelectPlaylistDialog(self, playlists=templates, session=session) dlg.exec() @@ -1031,7 +1028,7 @@ class Window(QMainWindow, Ui_MainWindow): def open_playlist(self) -> None: """Open existing playlist""" - with Session() as session: + with db.Session() as session: playlists = Playlists.get_closed(session) dlg = SelectPlaylistDialog(self, playlists=playlists, session=session) dlg.exec() @@ -1190,7 +1187,7 @@ class Window(QMainWindow, Ui_MainWindow): Rename current playlist """ - with Session() as session: + with db.Session() as session: playlist_id = self.active_tab().playlist_id playlist = session.get(Playlists, playlist_id) if playlist: @@ -1239,7 +1236,7 @@ class Window(QMainWindow, Ui_MainWindow): def save_as_template(self) -> None: """Save current playlist as template""" - with Session() as session: + with db.Session() as session: template_names = [a.name for a in Playlists.get_all_templates(session)] while True: @@ -1301,7 +1298,7 @@ class Window(QMainWindow, Ui_MainWindow): def set_main_window_size(self) -> None: """Set size of window from database""" - with Session() as session: + with db.Session() as session: settings = Settings.all_as_dict(session) record = settings["mainwindow_x"] x = record.f_int or 1 @@ -1742,18 +1739,15 @@ if __name__ == "__main__": # Run as required if args.check_db: - log.debug("Updating database") - with Session() as session: + log.debug("Checking database") + with db.Session() as session: check_db(session) - engine.dispose() elif args.update_bitrates: log.debug("Update bitrates") - with Session() as session: + with db.Session() as session: update_bitrates(session) - engine.dispose() else: try: - Base.metadata.create_all(engine) app = QApplication(sys.argv) # PyQt6 defaults to a grey for labels palette = app.palette() @@ -1771,7 +1765,6 @@ if __name__ == "__main__": win = Window() win.show() status = app.exec() - engine.dispose() sys.exit(status) except Exception as exc: if os.environ["MM_ENV"] == "PRODUCTION": diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 6408176..8d444c2 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -1,5 +1,14 @@ +# Standard library imports # Allow forward reference to PlaylistModel from __future__ import annotations + +# PyQt imports + +# Third party imports + +# App imports +from dbtables import db + import obsws_python as obs # type: ignore import re import datetime as dt @@ -25,7 +34,6 @@ from PyQt6.QtGui import ( from classes import track_sequence, MusicMusterSignals, PlaylistTrack from config import Config -from dbconfig import scoped_session, Session from helpers import ( file_is_unreadable, get_embedded_time, @@ -129,7 +137,7 @@ class PlaylistModel(QAbstractTableModel): self.signals.end_reset_model_signal.connect(self.end_reset_model) self.signals.row_order_changed_signal.connect(self.row_order_changed) - with Session() as session: + with db.Session() as session: # Ensure row numbers in playlist are contiguous PlaylistRows.fixup_rownumbers(session, playlist_id) # Populate self.playlist_rows @@ -165,7 +173,7 @@ class PlaylistModel(QAbstractTableModel): "Header row already has track associated" ) return - with Session() as session: + with db.Session() as session: plr = session.get(PlaylistRows, prd.plrid) if plr: # Add track to PlaylistRows @@ -187,7 +195,7 @@ class PlaylistModel(QAbstractTableModel): # Header row if self.is_header_row(row): # Check for specific header colouring - with Session() as session: + with db.Session() as session: note_colour = NoteColours.get_colour(session, prd.note) if note_colour: return QBrush(QColor(note_colour)) @@ -216,7 +224,7 @@ class PlaylistModel(QAbstractTableModel): return QBrush(QColor(Config.COLOUR_BITRATE_OK)) if column == Col.NOTE.value: if prd.note: - with Session() as session: + with db.Session() as session: note_colour = NoteColours.get_colour(session, prd.note) if note_colour: return QBrush(QColor(note_colour)) @@ -274,7 +282,7 @@ class PlaylistModel(QAbstractTableModel): # Check for OBS scene change self.obs_scene_change(row_number) - with Session() as session: + with db.Session() as session: # Update Playdates in database Playdates(session, track_sequence.now.track_id) @@ -372,7 +380,7 @@ class PlaylistModel(QAbstractTableModel): Delete from highest row back so that not yet deleted row numbers don't change. """ - with Session() as session: + with db.Session() as session: for row_number in sorted(row_numbers, reverse=True): log.info(f"delete_rows(), {row_number=}") super().beginRemoveRows(QModelIndex(), row_number, row_number) @@ -449,7 +457,7 @@ class PlaylistModel(QAbstractTableModel): if playlist_id != self.playlist_id: log.debug(f"end_reset_model: not us ({self.playlist_id=})") return - with Session() as session: + with db.Session() as session: self.refresh_data(session) super().endResetModel() self.reset_track_sequence_row_numbers() @@ -749,7 +757,7 @@ class PlaylistModel(QAbstractTableModel): new_row_number = self._get_new_row_number(proposed_row_number) - with Session() as session: + with db.Session() as session: super().beginInsertRows(QModelIndex(), new_row_number, new_row_number) plr = PlaylistRows.insert_row(session, self.playlist_id, new_row_number) @@ -815,7 +823,7 @@ class PlaylistModel(QAbstractTableModel): Mark row as unplayed """ - with Session() as session: + with db.Session() as session: for row_number in row_numbers: plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid) if not plr: @@ -878,7 +886,7 @@ class PlaylistModel(QAbstractTableModel): plrid = self.playlist_rows[oldrow].plrid sqla_map.append({"plrid": plrid, "plr_rownum": newrow}) - with Session() as session: + with db.Session() as session: PlaylistRows.update_plr_rownumbers(session, self.playlist_id, sqla_map) # Update playlist_rows self.refresh_data(session) @@ -907,7 +915,7 @@ class PlaylistModel(QAbstractTableModel): # Prepare destination playlist for a reset self.signals.begin_reset_model_signal.emit(to_playlist_id) - with Session() as session: + with db.Session() as session: # Make room in destination playlist max_destination_row_number = PlaylistRows.get_last_used_row( session, to_playlist_id @@ -957,7 +965,7 @@ class PlaylistModel(QAbstractTableModel): log.info(f"move_track_add_note({new_row_number=}, {existing_prd=}, {note=}") if note: - with Session() as session: + with db.Session() as session: plr = session.get(PlaylistRows, existing_prd.plrid) if plr: if plr.note: @@ -1045,7 +1053,7 @@ class PlaylistModel(QAbstractTableModel): # Update display self.invalidate_row(track_sequence.previous.plr_rownum) - def refresh_data(self, session: scoped_session): + def refresh_data(self, session: db.session): """Populate dicts for data calls""" # Populate self.playlist_rows with playlist data @@ -1066,7 +1074,7 @@ class PlaylistModel(QAbstractTableModel): log.info(f"remove_track({row_number=})") - with Session() as session: + with db.Session() as session: plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid) if plr: plr.track_id = None @@ -1080,7 +1088,7 @@ class PlaylistModel(QAbstractTableModel): track_id = self.playlist_rows[row_number].track_id if track_id: - with Session() as session: + with db.Session() as session: track = session.get(Tracks, track_id) set_track_metadata(track) self.refresh_row(session, row_number) @@ -1096,7 +1104,7 @@ class PlaylistModel(QAbstractTableModel): # Check the track_sequence next, now and previous plrs and # update the row number - with Session() as session: + with db.Session() as session: if track_sequence.next.plr_rownum: next_plr = session.get(PlaylistRows, track_sequence.next.plr_id) if next_plr: @@ -1201,7 +1209,7 @@ class PlaylistModel(QAbstractTableModel): return # Update playing_track - with Session() as session: + with db.Session() as session: track_sequence.next = PlaylistTrack() try: plrid = self.playlist_rows[row_number].plrid @@ -1241,7 +1249,7 @@ class PlaylistModel(QAbstractTableModel): row_number = index.row() column = index.column() - with Session() as session: + with db.Session() as session: plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid) if not plr: print( @@ -1450,7 +1458,7 @@ class PlaylistProxyModel(QSortFilterProxyModel): if self.source_model.played_tracks_hidden: if self.source_model.is_played_row(source_row): # Don't hide current or next track - with Session() as session: + with db.Session() as session: if track_sequence.next.plr_id: next_plr = session.get(PlaylistRows, track_sequence.next.plr_id) if ( diff --git a/app/playlists.py b/app/playlists.py index 170fc27..d9d25b2 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -1,8 +1,9 @@ +# Standard library imports +from typing import Callable, cast, List, Optional, TYPE_CHECKING import psutil import time -from pprint import pprint -from typing import Callable, cast, List, Optional, TYPE_CHECKING +# PyQt imports from PyQt6.QtCore import ( QEvent, QModelIndex, @@ -30,10 +31,13 @@ from PyQt6.QtWidgets import ( QStyleOption, ) -from dbconfig import Session -from dialogs import TrackSelectDialog +# Third party imports + +# App imports from classes import MusicMusterSignals, track_sequence from config import Config +from dbtables import db +from dialogs import TrackSelectDialog from helpers import ( ask_yes_no, ms_to_mmss, @@ -42,10 +46,9 @@ from helpers import ( ) from log import log from models import Settings - +from playlistmodel import PlaylistModel, PlaylistProxyModel if TYPE_CHECKING: from musicmuster import Window -from playlistmodel import PlaylistModel, PlaylistProxyModel class EscapeDelegate(QStyledItemDelegate): @@ -335,7 +338,7 @@ class PlaylistTab(QTableView): if model_row_number is None: return - with Session() as session: + with db.Session() as session: dlg = TrackSelectDialog( session=session, new_row_number=model_row_number, @@ -536,7 +539,7 @@ class PlaylistTab(QTableView): # Resize rows if necessary self.resizeRowsToContents() - with Session() as session: + with db.Session() as session: attr_name = f"playlist_col_{column_number}_width" record = Settings.get_int_settings(session, attr_name) record.f_int = self.columnWidth(column_number) @@ -830,7 +833,7 @@ class PlaylistTab(QTableView): return # Last column is set to stretch so ignore it here - with Session() as session: + with db.Session() as session: for column_number in range(header.count() - 1): attr_name = f"playlist_col_{column_number}_width" record = Settings.get_int_settings(session, attr_name) diff --git a/poetry.lock b/poetry.lock index 6a5cd67..23d7a09 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,6 +12,24 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "alchemical" +version = "1.0.1" +description = "Modern SQLAlchemy simplified" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "alchemical-1.0.1-py3-none-any.whl", hash = "sha256:79a98d459ed4f8afa8ed44b21d4d2ccf3586c76d73f53f647a9338aaba2bb33c"}, + {file = "alchemical-1.0.1.tar.gz", hash = "sha256:cff882cfef9533a56c53aa6bb38d5fa19939ea283226017c7c9369b73422200a"}, +] + +[package.dependencies] +sqlalchemy = ">=1.4.24" + +[package.extras] +docs = ["sphinx"] + [[package]] name = "alembic" version = "1.13.1" @@ -2273,4 +2291,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "500cefc31e30cba9ae917cc51b7407961d69825d1fcae53515ed1fa12f4ab171" +content-hash = "f4fb2696ae984283c4c0d7816ba7cbd7be714695d6eb3c84b5da62b3809f9c82" diff --git a/pyproject.toml b/pyproject.toml index d8840a4..3283311 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ pyqt6-webengine = "^6.5.0" pygame = "^2.4.0" pyqtgraph = "^0.13.3" colorlog = "^6.8.0" +alchemical = "^1.0.1" [tool.poetry.dev-dependencies] ipdb = "^0.13.9" From 3821a7061b9c632713db8d5ee07fc32a32e4969b Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Fri, 5 Apr 2024 08:37:36 +0100 Subject: [PATCH 3/7] Migrated to Alchemical --- .envrc | 2 +- app/config.py | 2 +- app/dbtables.py | 27 +- app/models.py | 113 +-- app/playlistmodel.py | 31 +- conftest.py | 49 -- poetry.lock | 345 +++++---- pyproject.toml | 3 +- .../X_test_playlists.py | 0 tests/test_helpers.py | 107 +-- tests/test_misc.py | 61 +- tests/test_models.py | 505 ++++++------ tests/test_playlistmodel.py | 729 +++++++++--------- 13 files changed, 1036 insertions(+), 938 deletions(-) delete mode 100644 conftest.py rename X_test_playlists.py => tests/X_test_playlists.py (100%) diff --git a/.envrc b/.envrc index 73a861a..2a6d79d 100644 --- a/.envrc +++ b/.envrc @@ -15,6 +15,6 @@ elif on_git_branch master; then export MM_DB="mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod" else export MM_ENV="DEVELOPMENT" - export MM_DB="mysql+mysqldb://dev_musicmuster:dev_musicmuster@localhost/dev_musicmuster" + export ALCHEMICAL_DATABASE_URI="mysql+mysqldb://dev_musicmuster:dev_musicmuster@localhost/dev_musicmuster" export PYTHONBREAKPOINT="pudb.set_trace" fi diff --git a/app/config.py b/app/config.py index 62c5d5f..55c16ca 100644 --- a/app/config.py +++ b/app/config.py @@ -35,7 +35,7 @@ class Config(object): COLOUR_WARNING_TIMER = "#ffc107" DBFS_SILENCE = -50 DEBUG_FUNCTIONS: List[Optional[str]] = [] - DEBUG_MODULES: List[Optional[str]] = ["dbconfig"] + DEBUG_MODULES: List[Optional[str]] = [] DEFAULT_COLUMN_WIDTH = 200 DISPLAY_SQL = False EPOCH = dt.datetime(1970, 1, 1) diff --git a/app/dbtables.py b/app/dbtables.py index 93b8e24..a1fe9cd 100644 --- a/app/dbtables.py +++ b/app/dbtables.py @@ -1,7 +1,8 @@ # Standard library imports +import os +import sys from typing import List, Optional import datetime as dt -import os # PyQt imports @@ -24,8 +25,6 @@ from sqlalchemy.orm import ( # Database classes -# Note: initialisation of the 'db' variable is at the foot of this -# module. class CartsTable(Model): __tablename__ = "carts" @@ -56,7 +55,7 @@ class NoteColoursTable(Model): def __repr__(self) -> str: return ( - f"" ) @@ -67,7 +66,7 @@ class PlaydatesTable(Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) lastplayed: Mapped[dt.datetime] = mapped_column(index=True) track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id")) - track: Mapped["TracksTable"] = relationship("Tracks", back_populates="playdates") + track: Mapped["TracksTable"] = relationship("TracksTable", back_populates="playdates") def __repr__(self) -> str: return ( @@ -91,10 +90,10 @@ class PlaylistsTable(Model): is_template: Mapped[bool] = mapped_column(default=False) deleted: Mapped[bool] = mapped_column(default=False) rows: Mapped[List["PlaylistRowsTable"]] = relationship( - "PlaylistRows", + "PlaylistRowsTable", back_populates="playlist", cascade="all, delete-orphan", - order_by="PlaylistRows.plr_rownum", + order_by="PlaylistRowsTable.plr_rownum", ) def __repr__(self) -> str: @@ -116,7 +115,7 @@ class PlaylistRowsTable(Model): playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows") track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id")) track: Mapped["TracksTable"] = relationship( - "Tracks", + "TracksTable", back_populates="playlistrows", ) played: Mapped[bool] = mapped_column( @@ -163,11 +162,11 @@ class TracksTable(Model): silence_at: Mapped[int] = mapped_column(index=False) start_gap: Mapped[int] = mapped_column(index=False) playlistrows: Mapped[List[PlaylistRowsTable]] = relationship( - "PlaylistRows", back_populates="track" + "PlaylistRowsTable", back_populates="track" ) playlists = association_proxy("playlistrows", "playlist") playdates: Mapped[List[PlaydatesTable]] = relationship( - "Playdates", + "PlaydatesTable", back_populates="track", lazy="joined", ) @@ -177,11 +176,3 @@ class TracksTable(Model): f"" ) - - -MYSQL_CONNECT = os.environ.get("MM_DB") -if MYSQL_CONNECT is None: - raise ValueError("MYSQL_CONNECT is undefined") -else: - dbname = MYSQL_CONNECT.split("/")[-1] -db = Alchemical(MYSQL_CONNECT) diff --git a/app/models.py b/app/models.py index 3b15c20..0d41bd3 100644 --- a/app/models.py +++ b/app/models.py @@ -1,11 +1,14 @@ # Standard library imports from typing import List, Optional, Sequence import datetime as dt +import os import re +import sys # PyQt imports # Third party imports +from alchemical import Alchemical # type:ignore from sqlalchemy import ( bindparam, delete, @@ -13,16 +16,10 @@ from sqlalchemy import ( select, update, ) -from sqlalchemy.exc import ( - IntegrityError, -) -from sqlalchemy.orm import ( - joinedload, - scoped_session, -) -from sqlalchemy.orm.exc import ( - NoResultFound, -) +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy.orm import joinedload +from sqlalchemy.orm.session import Session # App imports import dbtables @@ -30,12 +27,21 @@ from config import Config from log import log +# Establish database connection +ALCHEMICAL_DATABASE_URI = os.environ.get("ALCHEMICAL_DATABASE_URI") +if ALCHEMICAL_DATABASE_URI is None: + raise ValueError("ALCHEMICAL_DATABASE_URI is undefined") +if 'unittest' in sys.modules and 'sqlite' not in ALCHEMICAL_DATABASE_URI: + raise ValueError("Unit tests running on non-Sqlite database") +db = Alchemical(ALCHEMICAL_DATABASE_URI) + + # Database classes class Carts(dbtables.CartsTable): def __init__( self, - session: scoped_session, + session: Session, cart_number: int, name: str, duration: Optional[int] = None, @@ -58,7 +64,7 @@ class NoteColours(dbtables.NoteColoursTable): def __init__( self, - session: scoped_session, + session: Session, substring: str, colour: str, enabled: bool = True, @@ -77,7 +83,7 @@ class NoteColours(dbtables.NoteColoursTable): session.flush() @classmethod - def get_all(cls, session: scoped_session) -> Sequence["NoteColours"]: + def get_all(cls, session: Session) -> Sequence["NoteColours"]: """ Return all records """ @@ -85,7 +91,7 @@ class NoteColours(dbtables.NoteColoursTable): return session.scalars(select(cls)).all() @staticmethod - def get_colour(session: scoped_session, text: str) -> Optional[str]: + def get_colour(session: Session, text: str) -> Optional[str]: """ Parse text and return colour string if matched, else empty string """ @@ -118,7 +124,7 @@ class NoteColours(dbtables.NoteColoursTable): class Playdates(dbtables.PlaydatesTable): - def __init__(self, session: scoped_session, track_id: int) -> None: + def __init__(self, session: Session, track_id: int) -> None: """Record that track was played""" self.lastplayed = dt.datetime.now() @@ -127,7 +133,7 @@ class Playdates(dbtables.PlaydatesTable): session.commit() @staticmethod - def last_played(session: scoped_session, track_id: int) -> dt.datetime: + def last_played(session: Session, track_id: int) -> dt.datetime: """Return datetime track last played or None""" last_played = session.execute( @@ -145,7 +151,7 @@ class Playdates(dbtables.PlaydatesTable): return Config.EPOCH # pragma: no cover @staticmethod - def played_after(session: scoped_session, since: dt.datetime) -> Sequence["Playdates"]: + def played_after(session: Session, since: dt.datetime) -> Sequence["Playdates"]: """Return a list of Playdates objects since passed time""" return session.scalars( @@ -157,13 +163,13 @@ class Playdates(dbtables.PlaydatesTable): class Playlists(dbtables.PlaylistsTable): - def __init__(self, session: scoped_session, name: str): + def __init__(self, session: Session, name: str): self.name = name session.add(self) session.flush() @staticmethod - def clear_tabs(session: scoped_session, playlist_ids: List[int]) -> None: + def clear_tabs(session: Session, playlist_ids: List[int]) -> None: """ Make all tab records NULL """ @@ -183,7 +189,7 @@ class Playlists(dbtables.PlaylistsTable): @classmethod def create_playlist_from_template( - cls, session: scoped_session, template: "Playlists", playlist_name: str + cls, session: Session, template: "Playlists", playlist_name: str ) -> Optional["Playlists"]: """Create a new playlist from template""" @@ -197,7 +203,7 @@ class Playlists(dbtables.PlaylistsTable): return playlist - def delete(self, session: scoped_session) -> None: + def delete(self, session: Session) -> None: """ Mark as deleted """ @@ -206,7 +212,7 @@ class Playlists(dbtables.PlaylistsTable): session.flush() @classmethod - def get_all(cls, session: scoped_session) -> Sequence["Playlists"]: + def get_all(cls, session: Session) -> Sequence["Playlists"]: """Returns a list of all playlists ordered by last use""" return session.scalars( @@ -216,7 +222,7 @@ class Playlists(dbtables.PlaylistsTable): ).all() @classmethod - def get_all_templates(cls, session: scoped_session) -> Sequence["Playlists"]: + def get_all_templates(cls, session: Session) -> Sequence["Playlists"]: """Returns a list of all templates ordered by name""" return session.scalars( @@ -224,7 +230,7 @@ class Playlists(dbtables.PlaylistsTable): ).all() @classmethod - def get_closed(cls, session: scoped_session) -> Sequence["Playlists"]: + def get_closed(cls, session: Session) -> Sequence["Playlists"]: """Returns a list of all closed playlists ordered by last use""" return session.scalars( @@ -238,7 +244,7 @@ class Playlists(dbtables.PlaylistsTable): ).all() @classmethod - def get_open(cls, session: scoped_session) -> Sequence[Optional["Playlists"]]: + def get_open(cls, session: Session) -> Sequence[Optional["Playlists"]]: """ Return a list of loaded playlists ordered by tab. """ @@ -254,7 +260,7 @@ class Playlists(dbtables.PlaylistsTable): self.last_used = dt.datetime.now() @staticmethod - def name_is_available(session: scoped_session, name: str) -> bool: + def name_is_available(session: Session, name: str) -> bool: """ Return True if no playlist of this name exists else false. """ @@ -264,7 +270,7 @@ class Playlists(dbtables.PlaylistsTable): is None ) - def rename(self, session: scoped_session, new_name: str) -> None: + def rename(self, session: Session, new_name: str) -> None: """ Rename playlist """ @@ -274,7 +280,7 @@ class Playlists(dbtables.PlaylistsTable): @staticmethod def save_as_template( - session: scoped_session, playlist_id: int, template_name: str + session: Session, playlist_id: int, template_name: str ) -> None: """Save passed playlist as new template""" @@ -292,7 +298,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): def __init__( self, - session: scoped_session, + session: Session, playlist_id: int, row_number: int, note: str = "", @@ -305,7 +311,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): self.plr_rownum = row_number self.note = note session.add(self) - session.flush() + session.commit() def append_note(self, extra_note: str) -> None: """Append passed note to any existing note""" @@ -317,7 +323,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): self.note = extra_note @staticmethod - def copy_playlist(session: scoped_session, src_id: int, dst_id: int) -> None: + def copy_playlist(session: Session, src_id: int, dst_id: int) -> None: """Copy playlist entries""" src_rows = session.scalars( @@ -335,7 +341,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): @classmethod def deep_row( - cls, session: scoped_session, playlist_id: int, row_number: int + cls, session: Session, playlist_id: int, row_number: int ) -> "PlaylistRows": """ Return a playlist row that includes full track and lastplayed data for @@ -356,7 +362,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): @classmethod def deep_rows( - cls, session: scoped_session, playlist_id: int + cls, session: Session, playlist_id: int ) -> Sequence["PlaylistRows"]: """ Return a list of playlist rows that include full track and lastplayed data for @@ -375,7 +381,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): @staticmethod def delete_higher_rows( - session: scoped_session, playlist_id: int, maxrow: int + session: Session, playlist_id: int, maxrow: int ) -> None: """ Delete rows in given playlist that have a higher row number @@ -391,7 +397,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): session.flush() @staticmethod - def delete_row(session: scoped_session, playlist_id: int, row_number: int) -> None: + def delete_row(session: Session, playlist_id: int, row_number: int) -> None: """ Delete passed row in given playlist. """ @@ -404,7 +410,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): ) @staticmethod - def fixup_rownumbers(session: scoped_session, playlist_id: int) -> None: + def fixup_rownumbers(session: Session, playlist_id: int) -> None: """ Ensure the row numbers for passed playlist have no gaps """ @@ -423,7 +429,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): @classmethod def plrids_to_plrs( - cls, session: scoped_session, playlist_id: int, plr_ids: List[int] + cls, session: Session, playlist_id: int, plr_ids: List[int] ) -> Sequence["PlaylistRows"]: """ Take a list of PlaylistRows ids and return a list of corresponding @@ -439,7 +445,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): return plrs @staticmethod - def get_last_used_row(session: scoped_session, playlist_id: int) -> Optional[int]: + def get_last_used_row(session: Session, playlist_id: int) -> Optional[int]: """Return the last used row for playlist, or None if no rows""" return session.execute( @@ -450,7 +456,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): @staticmethod def get_track_plr( - session: scoped_session, track_id: int, playlist_id: int + session: Session, track_id: int, playlist_id: int ) -> Optional["PlaylistRows"]: """Return first matching PlaylistRows object or None""" @@ -465,7 +471,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): @classmethod def get_played_rows( - cls, session: scoped_session, playlist_id: int + cls, session: Session, playlist_id: int ) -> Sequence["PlaylistRows"]: """ For passed playlist, return a list of rows that @@ -483,7 +489,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): @classmethod def get_rows_with_tracks( cls, - session: scoped_session, + session: Session, playlist_id: int, ) -> Sequence["PlaylistRows"]: """ @@ -500,7 +506,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): @classmethod def get_unplayed_rows( - cls, session: scoped_session, playlist_id: int + cls, session: Session, playlist_id: int ) -> Sequence["PlaylistRows"]: """ For passed playlist, return a list of playlist rows that @@ -521,14 +527,14 @@ class PlaylistRows(dbtables.PlaylistRowsTable): @classmethod def insert_row( - cls, session: scoped_session, playlist_id: int, new_row_number: int + cls, session: Session, playlist_id: int, new_row_number: int ) -> "PlaylistRows": cls.move_rows_down(session, playlist_id, new_row_number, 1) return cls(session, playlist_id, new_row_number) @staticmethod def move_rows_down( - session: scoped_session, playlist_id: int, starting_row: int, move_by: int + session: Session, playlist_id: int, starting_row: int, move_by: int ) -> None: """ Create space to insert move_by additional rows by incremented row @@ -548,7 +554,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): @staticmethod def update_plr_rownumbers( - session: scoped_session, playlist_id: int, sqla_map: List[dict[str, int]] + session: Session, playlist_id: int, sqla_map: List[dict[str, int]] ) -> None: """ Take a {plrid: plr_rownum} dictionary and update the row numbers accordingly @@ -569,6 +575,11 @@ class PlaylistRows(dbtables.PlaylistRowsTable): class Settings(dbtables.SettingsTable): + def __init__(self, session: Session, name: str): + self.name = name + session.add(self) + session.flush() + @classmethod def all_as_dict(cls, session): """ @@ -584,7 +595,7 @@ class Settings(dbtables.SettingsTable): return result @classmethod - def get_int_settings(cls, session: scoped_session, name: str) -> "Settings": + def get_int_settings(cls, session: Session, name: str) -> "Settings": """Get setting for an integer or return new setting record""" try: @@ -593,7 +604,7 @@ class Settings(dbtables.SettingsTable): except NoResultFound: return Settings(session, name) - def update(self, session: scoped_session, data: dict) -> None: + def update(self, session: Session, data: dict) -> None: for key, value in data.items(): assert hasattr(self, key) setattr(self, key, value) @@ -610,7 +621,7 @@ class Tracks(dbtables.TracksTable): def __init__( self, - session: scoped_session, + session: Session, path: str, title: str, artist: str, @@ -646,7 +657,7 @@ class Tracks(dbtables.TracksTable): return session.scalars(select(cls)).unique().all() @classmethod - def get_by_path(cls, session: scoped_session, path: str) -> Optional["Tracks"]: + def get_by_path(cls, session: Session, path: str) -> Optional["Tracks"]: """ Return track with passed path, or None. """ @@ -661,7 +672,7 @@ class Tracks(dbtables.TracksTable): return None @classmethod - def search_artists(cls, session: scoped_session, text: str) -> Sequence["Tracks"]: + def search_artists(cls, session: Session, text: str) -> Sequence["Tracks"]: """ Search case-insenstively for artists containing str @@ -682,7 +693,7 @@ class Tracks(dbtables.TracksTable): ) @classmethod - def search_titles(cls, session: scoped_session, text: str) -> Sequence["Tracks"]: + def search_titles(cls, session: Session, text: str) -> Sequence["Tracks"]: """ Search case-insenstively for titles containing str diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 8d444c2..8a71d5e 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -2,21 +2,14 @@ # Allow forward reference to PlaylistModel from __future__ import annotations -# PyQt imports - -# Third party imports - -# App imports -from dbtables import db - -import obsws_python as obs # type: ignore -import re -import datetime as dt from enum import auto, Enum from operator import attrgetter from random import shuffle from typing import List, Optional +import datetime as dt +import re +# PyQt imports from PyQt6.QtCore import ( QAbstractTableModel, QModelIndex, @@ -32,6 +25,11 @@ from PyQt6.QtGui import ( QFont, ) +# Third party imports +import obsws_python as obs # type: ignore +# import snoop # type: ignore + +# App imports from classes import track_sequence, MusicMusterSignals, PlaylistTrack from config import Config from helpers import ( @@ -42,7 +40,7 @@ from helpers import ( set_track_metadata, ) from log import log -from models import NoteColours, Playdates, PlaylistRows, Tracks +from models import db, NoteColours, Playdates, PlaylistRows, Tracks HEADER_NOTES_COLUMN = 1 @@ -555,7 +553,7 @@ class PlaylistModel(QAbstractTableModel): else: new_row_number = proposed_row_number - log.info(f"get_new_row_number() return: {new_row_number=}") + log.debug(f"get_new_row_number() return: {new_row_number=}") return new_row_number def get_row_info(self, row_number: int) -> PlaylistRowData: @@ -753,7 +751,7 @@ class PlaylistModel(QAbstractTableModel): Insert a row. """ - log.info(f"insert_row({proposed_row_number=}, {track_id=}, {note=})") + log.debug(f"insert_row({proposed_row_number=}, {track_id=}, {note=})") new_row_number = self._get_new_row_number(proposed_row_number) @@ -1345,12 +1343,15 @@ class PlaylistModel(QAbstractTableModel): Update track start/end times in self.playlist_rows """ - log.info("update_track_times()") + log.debug("update_track_times()") next_start_time: Optional[dt.datetime] = None update_rows: List[int] = [] + playlist_length = len(self.playlist_rows) + if not playlist_length: + return - for row_number in range(len(self.playlist_rows)): + for row_number in range(playlist_length): prd = self.playlist_rows[row_number] # Reset start_time if this is the current row diff --git a/conftest.py b/conftest.py deleted file mode 100644 index b284c31..0000000 --- a/conftest.py +++ /dev/null @@ -1,49 +0,0 @@ -# https://itnext.io/setting-up-transactional-tests-with-pytest-and-sqlalchemy-b2d726347629 - -import pytest -import helpers - -from sqlalchemy import create_engine -from sqlalchemy.orm import scoped_session, sessionmaker - -from app.models import Base, Tracks - -DB_CONNECTION = "mysql+mysqldb://musicmuster_testing:musicmuster_testing@localhost/dev_musicmuster_testing" - - -@pytest.fixture(scope="session") -def db_engine(): - engine = create_engine(DB_CONNECTION, isolation_level="READ COMMITTED") - Base.metadata.create_all(engine) - yield engine - engine.dispose() - - -@pytest.fixture(scope="function") -def session(db_engine): - connection = db_engine.connect() - transaction = connection.begin() - sm = sessionmaker(bind=connection) - session = scoped_session(sm) - # print(f"PyTest SqlA: session acquired [{hex(id(session))}]") - yield session - # print(f" PyTest SqlA: session released and cleaned up [{hex(id(session))}]") - session.remove() - transaction.rollback() - connection.close() - - -@pytest.fixture(scope="function") -def track1(session): - track_path = "testdata/isa.mp3" - metadata = helpers.get_file_metadata(track_path) - track = Tracks(session, **metadata) - return track - - -@pytest.fixture(scope="function") -def track2(session): - track_path = "testdata/mom.mp3" - metadata = helpers.get_file_metadata(track_path) - track = Tracks(session, **metadata) - return track diff --git a/poetry.lock b/poetry.lock index 23d7a09..6df8782 100644 --- a/poetry.lock +++ b/poetry.lock @@ -108,34 +108,34 @@ lxml = ["lxml"] [[package]] name = "black" -version = "24.2.0" +version = "24.3.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, - {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, - {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, - {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, - {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, - {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, - {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, - {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, - {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, - {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, - {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, - {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, - {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, - {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, - {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, + {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, + {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, + {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, + {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, + {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, + {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, + {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, + {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, + {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, + {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, + {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, + {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, + {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, + {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, + {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, + {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, + {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, + {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, + {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, + {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, + {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, + {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, ] [package.dependencies] @@ -265,6 +265,21 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "cheap-repr" +version = "0.5.1" +description = "Better version of repr/reprlib for short, cheap string representations." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "cheap_repr-0.5.1-py2.py3-none-any.whl", hash = "sha256:30096998aeb49367a4a153988d7a99dce9dc59bbdd4b19740da6b4f3f97cf2ff"}, + {file = "cheap_repr-0.5.1.tar.gz", hash = "sha256:31ec63b9d8394aa23d746c8376c8307f75f9fca0b983566b8bcf13cc661fe6dd"}, +] + +[package.extras] +tests = ["Django", "Django (<2)", "Django (<3)", "chainmap", "numpy (>=1.16.3)", "numpy (>=1.16.3,<1.17)", "numpy (>=1.16.3,<1.19)", "pandas (>=0.24.2)", "pandas (>=0.24.2,<0.25)", "pandas (>=0.24.2,<0.26)", "pytest"] + [[package]] name = "click" version = "8.1.7" @@ -601,23 +616,23 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.0.1" +version = "7.1.0" description = "Read metadata from Python packages" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, - {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "iniconfig" @@ -965,39 +980,39 @@ files = [ [[package]] name = "mypy" -version = "1.8.0" +version = "1.9.0" description = "Optional static typing for Python" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, - {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, - {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, - {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, - {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, - {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, - {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, - {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, - {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, - {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, - {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, - {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, - {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, - {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, - {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, - {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, - {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, - {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, - {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, + {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, + {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, + {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, + {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, + {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, + {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, + {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, + {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, + {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, + {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, + {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, + {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, + {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, + {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, + {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, + {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, + {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, ] [package.dependencies] @@ -1109,14 +1124,14 @@ dev = ["black", "isort", "pytest", "pytest-randomly"] [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -1484,16 +1499,16 @@ PyQt6-sip = ">=13.6,<14" [[package]] name = "pyqt6-qt6" -version = "6.6.2" +version = "6.6.3" description = "The subset of a Qt installation needed by PyQt6." category = "main" optional = false python-versions = "*" files = [ - {file = "PyQt6_Qt6-6.6.2-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:7ef446d3ffc678a8586ff6dc9f0d27caf4dff05dea02c353540d2f614386faf9"}, - {file = "PyQt6_Qt6-6.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b8363d88623342a72ac17da9127dc12f259bb3148796ea029762aa2d499778d9"}, - {file = "PyQt6_Qt6-6.6.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:8d7f674a4ec43ca00191e14945ca4129acbe37a2172ed9d08214ad58b170bc11"}, - {file = "PyQt6_Qt6-6.6.2-py3-none-win_amd64.whl", hash = "sha256:5a41fe9d53b9e29e9ec5c23f3c5949dba160f90ca313ee8b96b8ffe6a5059387"}, + {file = "PyQt6_Qt6-6.6.3-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:1674d161ea49a36e9146fd652e789d413a246cc2455ac8bf9c76902b4bd3b986"}, + {file = "PyQt6_Qt6-6.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:18fe1fbbc709dcff5c513e3cac7b1d7b630fb189e6d32a1601f193d73d326f42"}, + {file = "PyQt6_Qt6-6.6.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:6ae465dfcbb819dae5e18e8c96abba735b5bb2f16c066497dda4b7ca17c066ce"}, + {file = "PyQt6_Qt6-6.6.3-py3-none-win_amd64.whl", hash = "sha256:dbe509eccc579f8818b2b2e8ba93e27986facdd1d4d83ef1c7d9bd47cdf32651"}, ] [[package]] @@ -1548,32 +1563,32 @@ PyQt6-WebEngine-Qt6 = ">=6.6.0" [[package]] name = "pyqt6-webengine-qt6" -version = "6.6.2" +version = "6.6.3" description = "The subset of a Qt installation needed by PyQt6-WebEngine." category = "main" optional = false python-versions = "*" files = [ - {file = "PyQt6_WebEngine_Qt6-6.6.2-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:27b1b6a6f4ea115b3dd300d2df906d542009d9eb0e62b05e6b7cb85dfe68e9c3"}, - {file = "PyQt6_WebEngine_Qt6-6.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2364dfa3a6e751ead71b7ba759081be677fcf1c6bbd8a2a2a250eb5f06432e8"}, - {file = "PyQt6_WebEngine_Qt6-6.6.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3da4db9ddd984b647d0b79fa10fc6cf65364dfe283cd702b12cb7164be2307cd"}, - {file = "PyQt6_WebEngine_Qt6-6.6.2-py3-none-win_amd64.whl", hash = "sha256:5d6f3ae521115cee77fea22b0248e7b219995390b951b51e4d519aef9c304ca8"}, + {file = "PyQt6_WebEngine_Qt6-6.6.3-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:4ce545accc5a58d62bde7ce18253a70b3970c28a24c94642ec89537352c23974"}, + {file = "PyQt6_WebEngine_Qt6-6.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a82308115193a6f220d6310453d1edbe30f1a8ac32c01fc813865319a2199959"}, + {file = "PyQt6_WebEngine_Qt6-6.6.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:87f636e23e9c1a1326bf91d273da6bdfed2f42fcc243e527e7b0dbc4f39e70dd"}, + {file = "PyQt6_WebEngine_Qt6-6.6.3-py3-none-win_amd64.whl", hash = "sha256:3d3e81db62f166f5fbc24b28660fe81c1be4390282bfb9bb48111f32a6bd0f51"}, ] [[package]] name = "pyqtgraph" -version = "0.13.3" +version = "0.13.4" description = "Scientific Graphics and GUI Library for Python" category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pyqtgraph-0.13.3-py3-none-any.whl", hash = "sha256:fdcc04ac4b32a7bedf1bf3cf74cbb93ab3ba5687791712bbfa8d0712377d2f2b"}, - {file = "pyqtgraph-0.13.3.tar.gz", hash = "sha256:58108d8411c7054e0841d8b791ee85e101fc296b9b359c0e01dde38a98ff2ace"}, + {file = "pyqtgraph-0.13.4-py3-none-any.whl", hash = "sha256:1dc9a786aa43cd787114366058dc3b4b8cb96a0e318f334720c7e6cc6c285940"}, + {file = "pyqtgraph-0.13.4.tar.gz", hash = "sha256:67b0d371405c4fd5f35afecfeb37d4b73bc118f187c52a965ed68d62f59b67b3"}, ] [package.dependencies] -numpy = ">=1.20.0" +numpy = ">=1.22.0" [[package]] name = "pyreadline3" @@ -1589,14 +1604,14 @@ files = [ [[package]] name = "pytest" -version = "7.4.4" +version = "8.1.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, ] [package.dependencies] @@ -1604,11 +1619,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.4,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -1736,19 +1751,19 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "69.1.1" +version = "69.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, - {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -1763,6 +1778,28 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "snoop" +version = "0.4.3" +description = "Powerful debugging tools for Python" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "snoop-0.4.3-py2.py3-none-any.whl", hash = "sha256:b7418581889ff78b29d9dc5ad4625c4c475c74755fb5cba82c693c6e32afadc0"}, + {file = "snoop-0.4.3.tar.gz", hash = "sha256:2e0930bb19ff0dbdaa6f5933f88e89ed5984210ea9f9de0e1d8231fa5c1c1f25"}, +] + +[package.dependencies] +asttokens = "*" +cheap-repr = ">=0.4.0" +executing = "*" +pygments = "*" +six = "*" + +[package.extras] +tests = ["Django", "birdseye", "littleutils", "numpy (>=1.16.5)", "pandas (>=0.24.2)", "pprintpp", "prettyprinter", "pytest", "pytest-order", "pytest-order (<=0.11.0)"] + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -1943,61 +1980,61 @@ test = ["pytest"] [[package]] name = "sqlalchemy" -version = "2.0.27" +version = "2.0.29" description = "Database Abstraction Library" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.27-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d04e579e911562f1055d26dab1868d3e0bb905db3bccf664ee8ad109f035618a"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa67d821c1fd268a5a87922ef4940442513b4e6c377553506b9db3b83beebbd8"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c7a596d0be71b7baa037f4ac10d5e057d276f65a9a611c46970f012752ebf2d"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:954d9735ee9c3fa74874c830d089a815b7b48df6f6b6e357a74130e478dbd951"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5cd20f58c29bbf2680039ff9f569fa6d21453fbd2fa84dbdb4092f006424c2e6"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:03f448ffb731b48323bda68bcc93152f751436ad6037f18a42b7e16af9e91c07"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-win32.whl", hash = "sha256:d997c5938a08b5e172c30583ba6b8aad657ed9901fc24caf3a7152eeccb2f1b4"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-win_amd64.whl", hash = "sha256:eb15ef40b833f5b2f19eeae65d65e191f039e71790dd565c2af2a3783f72262f"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c5bad7c60a392850d2f0fee8f355953abaec878c483dd7c3836e0089f046bf6"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3012ab65ea42de1be81fff5fb28d6db893ef978950afc8130ba707179b4284a"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbcd77c4d94b23e0753c5ed8deba8c69f331d4fd83f68bfc9db58bc8983f49cd"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d177b7e82f6dd5e1aebd24d9c3297c70ce09cd1d5d37b43e53f39514379c029c"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:680b9a36029b30cf063698755d277885d4a0eab70a2c7c6e71aab601323cba45"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1306102f6d9e625cebaca3d4c9c8f10588735ef877f0360b5cdb4fdfd3fd7131"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-win32.whl", hash = "sha256:5b78aa9f4f68212248aaf8943d84c0ff0f74efc65a661c2fc68b82d498311fd5"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-win_amd64.whl", hash = "sha256:15e19a84b84528f52a68143439d0c7a3a69befcd4f50b8ef9b7b69d2628ae7c4"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0de1263aac858f288a80b2071990f02082c51d88335a1db0d589237a3435fe71"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce850db091bf7d2a1f2fdb615220b968aeff3849007b1204bf6e3e50a57b3d32"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dfc936870507da96aebb43e664ae3a71a7b96278382bcfe84d277b88e379b18"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4fbe6a766301f2e8a4519f4500fe74ef0a8509a59e07a4085458f26228cd7cc"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4535c49d961fe9a77392e3a630a626af5baa967172d42732b7a43496c8b28876"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0fb3bffc0ced37e5aa4ac2416f56d6d858f46d4da70c09bb731a246e70bff4d5"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-win32.whl", hash = "sha256:7f470327d06400a0aa7926b375b8e8c3c31d335e0884f509fe272b3c700a7254"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-win_amd64.whl", hash = "sha256:f9374e270e2553653d710ece397df67db9d19c60d2647bcd35bfc616f1622dcd"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e97cf143d74a7a5a0f143aa34039b4fecf11343eed66538610debc438685db4a"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7b5a3e2120982b8b6bd1d5d99e3025339f7fb8b8267551c679afb39e9c7c7f1"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e36aa62b765cf9f43a003233a8c2d7ffdeb55bc62eaa0a0380475b228663a38f"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5ada0438f5b74c3952d916c199367c29ee4d6858edff18eab783b3978d0db16d"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b1d9d1bfd96eef3c3faedb73f486c89e44e64e40e5bfec304ee163de01cf996f"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-win32.whl", hash = "sha256:ca891af9f3289d24a490a5fde664ea04fe2f4984cd97e26de7442a4251bd4b7c"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-win_amd64.whl", hash = "sha256:fd8aafda7cdff03b905d4426b714601c0978725a19efc39f5f207b86d188ba01"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec1f5a328464daf7a1e4e385e4f5652dd9b1d12405075ccba1df842f7774b4fc"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad862295ad3f644e3c2c0d8b10a988e1600d3123ecb48702d2c0f26771f1c396"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48217be1de7d29a5600b5c513f3f7664b21d32e596d69582be0a94e36b8309cb"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e56afce6431450442f3ab5973156289bd5ec33dd618941283847c9fd5ff06bf"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:611068511b5531304137bcd7fe8117c985d1b828eb86043bd944cebb7fae3910"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b86abba762ecfeea359112b2bb4490802b340850bbee1948f785141a5e020de8"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-win32.whl", hash = "sha256:30d81cc1192dc693d49d5671cd40cdec596b885b0ce3b72f323888ab1c3863d5"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-win_amd64.whl", hash = "sha256:120af1e49d614d2525ac247f6123841589b029c318b9afbfc9e2b70e22e1827d"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d07ee7793f2aeb9b80ec8ceb96bc8cc08a2aec8a1b152da1955d64e4825fcbac"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb0845e934647232b6ff5150df37ceffd0b67b754b9fdbb095233deebcddbd4a"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fc19ae2e07a067663dd24fca55f8ed06a288384f0e6e3910420bf4b1270cc51"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b90053be91973a6fb6020a6e44382c97739736a5a9d74e08cc29b196639eb979"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2f5c9dfb0b9ab5e3a8a00249534bdd838d943ec4cfb9abe176a6c33408430230"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33e8bde8fff203de50399b9039c4e14e42d4d227759155c21f8da4a47fc8053c"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-win32.whl", hash = "sha256:d873c21b356bfaf1589b89090a4011e6532582b3a8ea568a00e0c3aab09399dd"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-win_amd64.whl", hash = "sha256:ff2f1b7c963961d41403b650842dc2039175b906ab2093635d8319bef0b7d620"}, - {file = "SQLAlchemy-2.0.27-py3-none-any.whl", hash = "sha256:1ab4e0448018d01b142c916cc7119ca573803a4745cfe341b8f95657812700ac"}, - {file = "SQLAlchemy-2.0.27.tar.gz", hash = "sha256:86a6ed69a71fe6b88bf9331594fa390a2adda4a49b5c06f98e47bf0d392534f8"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c142852ae192e9fe5aad5c350ea6befe9db14370b34047e1f0f7cf99e63c63b"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:99a1e69d4e26f71e750e9ad6fdc8614fbddb67cfe2173a3628a2566034e223c7"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef3fbccb4058355053c51b82fd3501a6e13dd808c8d8cd2561e610c5456013c"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d6753305936eddc8ed190e006b7bb33a8f50b9854823485eed3a886857ab8d1"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0f3ca96af060a5250a8ad5a63699180bc780c2edf8abf96c58af175921df847a"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c4520047006b1d3f0d89e0532978c0688219857eb2fee7c48052560ae76aca1e"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-win32.whl", hash = "sha256:b2a0e3cf0caac2085ff172c3faacd1e00c376e6884b5bc4dd5b6b84623e29e4f"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-win_amd64.whl", hash = "sha256:01d10638a37460616708062a40c7b55f73e4d35eaa146781c683e0fa7f6c43fb"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:308ef9cb41d099099fffc9d35781638986870b29f744382904bf9c7dadd08513"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:296195df68326a48385e7a96e877bc19aa210e485fa381c5246bc0234c36c78e"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a13b917b4ffe5a0a31b83d051d60477819ddf18276852ea68037a144a506efb9"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f6d971255d9ddbd3189e2e79d743ff4845c07f0633adfd1de3f63d930dbe673"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:61405ea2d563407d316c63a7b5271ae5d274a2a9fbcd01b0aa5503635699fa1e"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de7202ffe4d4a8c1e3cde1c03e01c1a3772c92858837e8f3879b497158e4cb44"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-win32.whl", hash = "sha256:b5d7ed79df55a731749ce65ec20d666d82b185fa4898430b17cb90c892741520"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-win_amd64.whl", hash = "sha256:205f5a2b39d7c380cbc3b5dcc8f2762fb5bcb716838e2d26ccbc54330775b003"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d96710d834a6fb31e21381c6d7b76ec729bd08c75a25a5184b1089141356171f"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52de4736404e53c5c6a91ef2698c01e52333988ebdc218f14c833237a0804f1b"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c7b02525ede2a164c5fa5014915ba3591730f2cc831f5be9ff3b7fd3e30958e"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dfefdb3e54cd15f5d56fd5ae32f1da2d95d78319c1f6dfb9bcd0eb15d603d5d"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a88913000da9205b13f6f195f0813b6ffd8a0c0c2bd58d499e00a30eb508870c"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fecd5089c4be1bcc37c35e9aa678938d2888845a134dd016de457b942cf5a758"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-win32.whl", hash = "sha256:8197d6f7a3d2b468861ebb4c9f998b9df9e358d6e1cf9c2a01061cb9b6cf4e41"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-win_amd64.whl", hash = "sha256:9b19836ccca0d321e237560e475fd99c3d8655d03da80c845c4da20dda31b6e1"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:87a1d53a5382cdbbf4b7619f107cc862c1b0a4feb29000922db72e5a66a5ffc0"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0732dffe32333211801b28339d2a0babc1971bc90a983e3035e7b0d6f06b93"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90453597a753322d6aa770c5935887ab1fc49cc4c4fdd436901308383d698b4b"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ea311d4ee9a8fa67f139c088ae9f905fcf0277d6cd75c310a21a88bf85e130f5"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5f20cb0a63a3e0ec4e169aa8890e32b949c8145983afa13a708bc4b0a1f30e03"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-win32.whl", hash = "sha256:e5bbe55e8552019c6463709b39634a5fc55e080d0827e2a3a11e18eb73f5cdbd"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-win_amd64.whl", hash = "sha256:c2f9c762a2735600654c654bf48dad388b888f8ce387b095806480e6e4ff6907"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e614d7a25a43a9f54fcce4675c12761b248547f3d41b195e8010ca7297c369c"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:471fcb39c6adf37f820350c28aac4a7df9d3940c6548b624a642852e727ea586"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:988569c8732f54ad3234cf9c561364221a9e943b78dc7a4aaf35ccc2265f1930"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dddaae9b81c88083e6437de95c41e86823d150f4ee94bf24e158a4526cbead01"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:334184d1ab8f4c87f9652b048af3f7abea1c809dfe526fb0435348a6fef3d380"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:38b624e5cf02a69b113c8047cf7f66b5dfe4a2ca07ff8b8716da4f1b3ae81567"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-win32.whl", hash = "sha256:bab41acf151cd68bc2b466deae5deeb9e8ae9c50ad113444151ad965d5bf685b"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-win_amd64.whl", hash = "sha256:52c8011088305476691b8750c60e03b87910a123cfd9ad48576d6414b6ec2a1d"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3071ad498896907a5ef756206b9dc750f8e57352113c19272bdfdc429c7bd7de"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dba622396a3170974f81bad49aacebd243455ec3cc70615aeaef9e9613b5bca5"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b184e3de58009cc0bf32e20f137f1ec75a32470f5fede06c58f6c355ed42a72"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c37f1050feb91f3d6c32f864d8e114ff5545a4a7afe56778d76a9aec62638ba"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bda7ce59b06d0f09afe22c56714c65c957b1068dee3d5e74d743edec7daba552"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:25664e18bef6dc45015b08f99c63952a53a0a61f61f2e48a9e70cec27e55f699"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-win32.whl", hash = "sha256:77d29cb6c34b14af8a484e831ab530c0f7188f8efed1c6a833a2c674bf3c26ec"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-win_amd64.whl", hash = "sha256:04c487305ab035a9548f573763915189fc0fe0824d9ba28433196f8436f1449c"}, + {file = "SQLAlchemy-2.0.29-py3-none-any.whl", hash = "sha256:dc4ee2d4ee43251905f88637d5281a8d52e916a021384ec10758826f5cbae305"}, + {file = "SQLAlchemy-2.0.29.tar.gz", hash = "sha256:bd9566b8e58cabd700bc367b60e90d9349cd16f0984973f98a9a09f9c64e86f0"}, ] [package.dependencies] @@ -2051,14 +2088,14 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] [[package]] name = "stackprinter" -version = "0.2.11" +version = "0.2.12" description = "Debug-friendly stack traces, with variable values and semantic highlighting" category = "main" optional = false python-versions = ">=3.4" files = [ - {file = "stackprinter-0.2.11-py3-none-any.whl", hash = "sha256:101da55db7dfd54af516e3e209db9c84645285e5ea00d0b0709418dde2f157a1"}, - {file = "stackprinter-0.2.11.tar.gz", hash = "sha256:abbd8f4f892f24a5bd370119af49c3e3408b0bf04cd4d28e99f81c4e781a767b"}, + {file = "stackprinter-0.2.12-py3-none-any.whl", hash = "sha256:0a0623d46a5babd7a8a9787f605f4dd4a42d6ff7aee140541d5e9291a506e8d9"}, + {file = "stackprinter-0.2.12.tar.gz", hash = "sha256:271efc75ebdcc1554e58168ea7779f98066d54a325f57c7dc19f10fa998ef01e"}, ] [[package]] @@ -2144,30 +2181,30 @@ files = [ [[package]] name = "traitlets" -version = "5.14.1" +version = "5.14.2" description = "Traitlets Python configuration system" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"}, - {file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"}, + {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"}, + {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.1)", "pytest-mock", "pytest-mypy-testing"] [[package]] name = "types-psutil" -version = "5.9.5.20240205" +version = "5.9.5.20240316" description = "Typing stubs for psutil" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "types-psutil-5.9.5.20240205.tar.gz", hash = "sha256:51df36a361aa597bf483dcc5b58f2ab7aa87452a36d2da97c90994d6a81ef743"}, - {file = "types_psutil-5.9.5.20240205-py3-none-any.whl", hash = "sha256:3ec9bd8b95a64fe1269241d3ffb74b94a45df2d0391da1402423cd33f29745ca"}, + {file = "types-psutil-5.9.5.20240316.tar.gz", hash = "sha256:5636f5714bb930c64bb34c4d47a59dc92f9d610b778b5364a31daa5584944848"}, + {file = "types_psutil-5.9.5.20240316-py3-none-any.whl", hash = "sha256:2fdd64ea6e97befa546938f486732624f9255fde198b55e6f00fda236f059f64"}, ] [[package]] @@ -2202,14 +2239,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "urwid" -version = "2.6.7" +version = "2.6.10" description = "A full-featured console (xterm et al.) user interface library" category = "dev" optional = false python-versions = ">3.7" files = [ - {file = "urwid-2.6.7-py3-none-any.whl", hash = "sha256:80b922d2051db6abe598b7e1b0b31d8d04fcc56d35bb1ec40b3c128fa0bd23ab"}, - {file = "urwid-2.6.7.tar.gz", hash = "sha256:597fa2d19ac788e4607d2a48aca32f257342201cb55e5f6a00a8fcd24e62a5ab"}, + {file = "urwid-2.6.10-py3-none-any.whl", hash = "sha256:f5d290ab01a9cf69a062d5d04ff69111903d41fc14ed03f3ed92cb36f5ef4735"}, + {file = "urwid-2.6.10.tar.gz", hash = "sha256:ae33355c414c13214e541d3634f3c8a0bfb373914e62ffbcf2fa863527706321"}, ] [package.dependencies] @@ -2274,21 +2311,21 @@ test = ["websockets"] [[package]] name = "zipp" -version = "3.17.0" +version = "3.18.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "f4fb2696ae984283c4c0d7816ba7cbd7be714695d6eb3c84b5da62b3809f9c82" +content-hash = "e8a4a3f4b5dd70bd5fb2ab420b4de6e3304a15be383233bb01b966e047700cd1" diff --git a/pyproject.toml b/pyproject.toml index 3283311..e5fd144 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ alchemical = "^1.0.1" [tool.poetry.dev-dependencies] ipdb = "^0.13.9" -pytest = "^7.0.1" pytest-qt = "^4.0.2" pydub-stubs = "^0.25.1" line-profiler = "^4.0.2" @@ -46,6 +45,8 @@ flakehell = "^0.9.0" mypy = "^1.7.0" pdbp = "^1.5.0" pytest-cov = "^5.0.0" +pytest = "^8.1.1" +snoop = "^0.4.3" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/X_test_playlists.py b/tests/X_test_playlists.py similarity index 100% rename from X_test_playlists.py rename to tests/X_test_playlists.py diff --git a/tests/test_helpers.py b/tests/test_helpers.py index f2ddecd..08cf6df 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,4 +1,12 @@ +# Standard library imports import datetime as dt +import unittest + +# PyQt imports + +# Third party imports + +# App imports from helpers import ( fade_point, get_audio_segment, @@ -9,68 +17,71 @@ from helpers import ( ) -def test_fade_point(): - test_track_path = "testdata/isa.mp3" - test_track_data = "testdata/isa.py" +class TestMMHelpers(unittest.TestCase): + def setUp(self): + pass - audio_segment = get_audio_segment(test_track_path) - assert audio_segment + def tearDown(self): + pass - fade_at = fade_point(audio_segment) + def test_fade_point(self): + test_track_path = "testdata/isa.mp3" + test_track_data = "testdata/isa.py" - # Get test data - with open(test_track_data) as f: - testdata = eval(f.read()) + audio_segment = get_audio_segment(test_track_path) + assert audio_segment - # Volume detection can vary, so ± 1 second is OK - assert fade_at < testdata["fade_at"] + 1000 - assert fade_at > testdata["fade_at"] - 1000 + fade_at = fade_point(audio_segment) + # Get test data + with open(test_track_data) as f: + testdata = eval(f.read()) -def test_get_tags(): - test_track_path = "testdata/mom.mp3" - test_track_data = "testdata/mom.py" + # Volume detection can vary, so ± 1 second is OK + assert fade_at < testdata["fade_at"] + 1000 + assert fade_at > testdata["fade_at"] - 1000 - tags = get_tags(test_track_path) + def test_get_tags(self): + test_track_path = "testdata/mom.mp3" + test_track_data = "testdata/mom.py" - # Get test data - with open(test_track_data) as f: - testdata = eval(f.read()) + tags = get_tags(test_track_path) - assert tags["artist"] == testdata["artist"] - assert tags["title"] == testdata["title"] + # Get test data + with open(test_track_data) as f: + testdata = eval(f.read()) + assert tags["artist"] == testdata["artist"] + assert tags["title"] == testdata["title"] -def test_get_relative_date(): - assert get_relative_date(None) == "Never" - today_at_10 = dt.datetime.now().replace(hour=10, minute=0) - today_at_11 = dt.datetime.now().replace(hour=11, minute=0) - assert get_relative_date(today_at_10, today_at_11) == "Today 10:00" - eight_days_ago = today_at_10 - dt.timedelta(days=8) - assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago" - sixteen_days_ago = today_at_10 - dt.timedelta(days=16) - assert get_relative_date(sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago" + def test_get_relative_date(self): + assert get_relative_date(None) == "Never" + today_at_10 = dt.datetime.now().replace(hour=10, minute=0) + today_at_11 = dt.datetime.now().replace(hour=11, minute=0) + assert get_relative_date(today_at_10, today_at_11) == "Today 10:00" + eight_days_ago = today_at_10 - dt.timedelta(days=8) + assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago" + sixteen_days_ago = today_at_10 - dt.timedelta(days=16) + assert get_relative_date(sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago" + def test_leading_silence(self): + test_track_path = "testdata/isa.mp3" + test_track_data = "testdata/isa.py" -def test_leading_silence(): - test_track_path = "testdata/isa.mp3" - test_track_data = "testdata/isa.py" + audio_segment = get_audio_segment(test_track_path) + assert audio_segment - audio_segment = get_audio_segment(test_track_path) - assert audio_segment + silence_at = leading_silence(audio_segment) - silence_at = leading_silence(audio_segment) + # Get test data + with open(test_track_data) as f: + testdata = eval(f.read()) - # Get test data - with open(test_track_data) as f: - testdata = eval(f.read()) + # Volume detection can vary, so ± 1 second is OK + assert silence_at < testdata["leading_silence"] + 1000 + assert silence_at > testdata["leading_silence"] - 1000 - # Volume detection can vary, so ± 1 second is OK - assert silence_at < testdata["leading_silence"] + 1000 - assert silence_at > testdata["leading_silence"] - 1000 - - -def test_ms_to_mmss(): - assert ms_to_mmss(None) == "-" - assert ms_to_mmss(59600) == "0:59" - assert ms_to_mmss((5 * 60 * 1000) + 23000) == "5:23" + def test_ms_to_mmss(self): + assert ms_to_mmss(None) == "-" + assert ms_to_mmss(59600) == "0:59" + assert ms_to_mmss((5 * 60 * 1000) + 23000) == "5:23" diff --git a/tests/test_misc.py b/tests/test_misc.py index 68b5d88..4fdff4d 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,25 +1,50 @@ +# Standard library imports +import os +import unittest + +# PyQt imports + +# Third party imports import pytest -from models import NoteColours, Settings + +# App imports +# Set up test database before importing db +# Mark subsequent lines to ignore E402, imports not at top of file +# Set up test database before importing db +# Mark subsequent lines to ignore E402, imports not at top of file +DB_FILE = '/tmp/mm.db' +if os.path.exists(DB_FILE): + os.unlink(DB_FILE) +os.environ['ALCHEMICAL_DATABASE_URI'] = 'sqlite:///' + DB_FILE +from models import db, Settings # noqa: E402 -def test_log_exception(): - """Test deliberate exception""" +class TestMMMisc(unittest.TestCase): + def setUp(self): + db.create_all() - with pytest.raises(Exception): - 1 / 0 + def tearDown(self): + db.drop_all() + def test_log_exception(self): + """Test deliberate exception""" -def test_create_settings(session): - SETTING_NAME = "wombat" - NO_SUCH_SETTING = "abc" - VALUE = 3 + with pytest.raises(Exception): + 1 / 0 - setting = Settings(session, SETTING_NAME) - setting.update(session, dict(f_int=VALUE)) - print(setting) - _ = Settings.all_as_dict(session) - test = Settings.get_int_settings(session, SETTING_NAME) - assert test.name == SETTING_NAME - assert test.f_int == VALUE - test_new = Settings.get_int_settings(session, NO_SUCH_SETTING) - assert test_new.name == NO_SUCH_SETTING + def test_create_settings(self): + SETTING_NAME = "wombat" + NO_SUCH_SETTING = "abc" + VALUE = 3 + + with db.Session() as session: + setting = Settings(session, SETTING_NAME) + # test repr + _ = str(setting) + setting.update(session, dict(f_int=VALUE)) + _ = Settings.all_as_dict(session) + test = Settings.get_int_settings(session, SETTING_NAME) + assert test.name == SETTING_NAME + assert test.f_int == VALUE + test_new = Settings.get_int_settings(session, NO_SUCH_SETTING) + assert test_new.name == NO_SUCH_SETTING diff --git a/tests/test_models.py b/tests/test_models.py index 8306ff2..7c4dc3b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,6 +1,23 @@ +# Standard library imports import datetime as dt +import os +import unittest -from app.models import ( +# PyQt imports + +# Third party imports + +# App imports +from app import helpers + +# Set up test database before importing db +# Mark subsequent lines to ignore E402, imports not at top of file +DB_FILE = '/tmp/mm.db' +if os.path.exists(DB_FILE): + os.unlink(DB_FILE) +os.environ['ALCHEMICAL_DATABASE_URI'] = 'sqlite:///' + DB_FILE +from app.models import ( # noqa: E402 + db, Carts, NoteColours, Playdates, @@ -10,241 +27,275 @@ from app.models import ( ) -def test_notecolours_get_colour(session): - """Create a colour record and retrieve all colours""" +class TestMMModels(unittest.TestCase): + def setUp(self): + db.create_all() + + with db.Session() as session: + track1_path = "testdata/isa.mp3" + metadata1 = helpers.get_file_metadata(track1_path) + self.track1 = Tracks(session, **metadata1) + # Test repr + _ = str(self.track1) + + track2_path = "testdata/mom.mp3" + metadata2 = helpers.get_file_metadata(track2_path) + self.track2 = Tracks(session, **metadata2) + + def tearDown(self): + db.drop_all() + + def test_notecolours_get_colour(self): + """Create a colour record and retrieve all colours""" + + note_colour = "#0bcdef" + with db.Session() as session: + NoteColours(session, substring="substring", colour=note_colour) + + records = NoteColours.get_all(session) + assert len(records) == 1 + record = records[0] + assert record.colour == note_colour + + def test_notecolours_get_all(self): + """Create two colour records and retrieve them all""" + + note1_colour = "#1bcdef" + note2_colour = "#20ff00" + with db.Session() as session: + NoteColours(session, substring="note1", colour=note1_colour) + NoteColours(session, substring="note2", colour=note2_colour) + + records = NoteColours.get_all(session) + assert len(records) == 2 + assert note1_colour in [n.colour for n in records] + assert note2_colour in [n.colour for n in records] + + def test_notecolours_get_colour_none(self): + note_colour = "#3bcdef" + with db.Session() as session: + NoteColours(session, substring="substring", colour=note_colour) + + result = NoteColours.get_colour(session, "xyz") + assert result is None + + def test_notecolours_get_colour_match(self): + note_colour = "#4bcdef" + with db.Session() as session: + nc = NoteColours(session, substring="sub", colour=note_colour) + assert nc + + result = NoteColours.get_colour(session, "The substring") + assert result == note_colour + + def test_playdates_add_playdate(self): + """Test playdate and last_played retrieval""" + + with db.Session() as session: + session.add(self.track1) + playdate = Playdates(session, self.track1.id) + assert playdate + # test repr + _ = str(playdate) + + last_played = Playdates.last_played(session, self.track1.id) + assert abs((playdate.lastplayed - last_played).total_seconds()) < 2 + + def test_playdates_played_after(self): + with db.Session() as session: + session.add(self.track1) + playdate = Playdates(session, self.track1.id) + yesterday = dt.datetime.now() - dt.timedelta(days=1) + played = Playdates.played_after(session, yesterday) + + assert len(played) == 1 + assert played[0] == playdate + + def test_playlist_create(self): + TEMPLATE_NAME = "my template" + + with db.Session() as session: + playlist = Playlists(session, "my playlist") + assert playlist + # test repr + _ = str(playlist) + + # test clear tabs + Playlists.clear_tabs(session, [playlist.id]) + + # create template + Playlists.save_as_template(session, playlist.id, TEMPLATE_NAME) + + # test create template + _ = Playlists.create_playlist_from_template( + session, playlist, "my new name" + ) + + # get all templates + all_templates = Playlists.get_all_templates(session) + assert len(all_templates) == 1 + # Save as template creates new playlist + assert all_templates[0] != playlist + # test delete playlist + playlist.delete(session) + + def test_playlist_open_and_close(self): + # We need a playlist + with db.Session() as session: + playlist = Playlists(session, "my playlist") + + assert len(Playlists.get_open(session)) == 0 + assert len(Playlists.get_closed(session)) == 1 + + playlist.mark_open() + + assert len(Playlists.get_open(session)) == 1 + assert len(Playlists.get_closed(session)) == 0 + + playlist.close() + + assert len(Playlists.get_open(session)) == 0 + assert len(Playlists.get_closed(session)) == 1 + + def test_playlist_get_all_and_by_id(self): + # We need two playlists + p1_name = "playlist one" + p2_name = "playlist two" + with db.Session() as session: + playlist1 = Playlists(session, p1_name) + _ = Playlists(session, p2_name) + + all_playlists = Playlists.get_all(session) + assert len(all_playlists) == 2 + assert p1_name in [p.name for p in all_playlists] + assert p2_name in [p.name for p in all_playlists] + assert session.get(Playlists, playlist1.id).name == p1_name + + def test_tracks_get_all_tracks(self): + # Need two tracks + + with db.Session() as session: + session.add(self.track1) + session.add(self.track2) + result = [a.path for a in Tracks.get_all(session)] + assert self.track1.path in result + assert self.track2.path in result + + def test_tracks_by_path(self): + with db.Session() as session: + session.add(self.track1) + assert Tracks.get_by_path(session, self.track1.path) is self.track1 + + def test_tracks_by_id(self): + with db.Session() as session: + session.add(self.track1) + assert session.get(Tracks, self.track1.id) is self.track1 + + def test_tracks_search_artists(self): + track1_artist = "Fleetwood Mac" + + with db.Session() as session: + session.add(self.track1) + assert len(Tracks.search_artists(session, track1_artist)) == 1 + + def test_tracks_search_titles(self): + track1_title = "I'm So Afraid" + + with db.Session() as session: + session.add(self.track1) + assert len(Tracks.search_titles(session, track1_title)) == 1 + + def test_repr(self): + """Just check for error retrieving reprs""" + + with db.Session() as session: + nc = NoteColours(session, substring="x", colour="x") + _ = str(nc) + + def test_get_colour(self): + """Test for errors in execution""" + + GOOD_STRING = "cantelope" + BAD_STRING = "ericTheBee" + SUBSTR = "ant" + COLOUR = "blue" + + with db.Session() as session: + nc1 = NoteColours( + session, substring=SUBSTR, colour=COLOUR, is_casesensitive=True + ) + + _ = nc1.get_colour(session, "") + colour = nc1.get_colour(session, GOOD_STRING) + assert colour == COLOUR + + colour = nc1.get_colour(session, BAD_STRING) + assert colour is None + + nc2 = NoteColours( + session, substring=".*" + SUBSTR, colour=COLOUR, is_regex=True + ) + colour = nc2.get_colour(session, GOOD_STRING) + assert colour == COLOUR - print(">>>text_notcolours_get_colour") - note_colour = "#0bcdef" - NoteColours(session, substring="substring", colour=note_colour) + colour = nc2.get_colour(session, BAD_STRING) + assert colour is None - records = NoteColours.get_all(session) - assert len(records) == 1 - record = records[0] - assert record.colour == note_colour + nc3 = NoteColours( + session, + substring=".*" + SUBSTR, + colour=COLOUR, + is_regex=True, + is_casesensitive=True, + ) + colour = nc3.get_colour(session, GOOD_STRING) + assert colour == COLOUR + + colour = nc3.get_colour(session, BAD_STRING) + assert colour is None + + def test_create_cart(self): + with db.Session() as session: + cart = Carts(session, 1, "name") + assert cart + _ = str(cart) + + def test_name_available(self): + PLAYLIST_NAME = "a name" + RENAME = "new name" -def test_notecolours_get_all(session): - """Create two colour records and retrieve them all""" + with db.Session() as session: + if Playlists.name_is_available(session, PLAYLIST_NAME): + playlist = Playlists(session, PLAYLIST_NAME) + assert playlist - print(">>>text_notcolours_get_all") - note1_colour = "#1bcdef" - note2_colour = "#20ff00" - NoteColours(session, substring="note1", colour=note1_colour) - NoteColours(session, substring="note2", colour=note2_colour) + assert Playlists.name_is_available(session, PLAYLIST_NAME) is False + + playlist.rename(session, RENAME) - records = NoteColours.get_all(session) - assert len(records) == 2 - assert note1_colour in [n.colour for n in records] - assert note2_colour in [n.colour for n in records] + def test_create_playlist_row(self): + PLAYLIST_NAME = "a name" + with db.Session() as session: + if Playlists.name_is_available(session, PLAYLIST_NAME): + playlist = Playlists(session, PLAYLIST_NAME) -def test_notecolours_get_colour_none(session): - note_colour = "#3bcdef" - NoteColours(session, substring="substring", colour=note_colour) + plr = PlaylistRows(session, playlist.id, 1) + assert plr + _ = str(plr) + plr.append_note("a note") + plr.append_note("another note") - result = NoteColours.get_colour(session, "xyz") - assert result is None + def test_delete_plr(self): + PLAYLIST_NAME = "a name" + with db.Session() as session: + if Playlists.name_is_available(session, PLAYLIST_NAME): + playlist = Playlists(session, PLAYLIST_NAME) -def test_notecolours_get_colour_match(session): - note_colour = "#4bcdef" - nc = NoteColours(session, substring="sub", colour=note_colour) - assert nc - - result = NoteColours.get_colour(session, "The substring") - assert result == note_colour - - -def test_playdates_add_playdate(session, track1): - """Test playdate and last_played retrieval""" - - playdate = Playdates(session, track1.id) - assert playdate - print(playdate) - - last_played = Playdates.last_played(session, track1.id) - assert abs((playdate.lastplayed - last_played).total_seconds()) < 2 - - -def test_playdates_played_after(session, track1): - playdate = Playdates(session, track1.id) - yesterday = dt.datetime.now() - dt.timedelta(days=1) - played = Playdates.played_after(session, yesterday) - - assert len(played) == 1 - assert played[0] == playdate - - -def test_playlist_create(session): - TEMPLATE_NAME = "my template" - - playlist = Playlists(session, "my playlist") - assert playlist - print(playlist) - - # test clear tabs - Playlists.clear_tabs(session, [playlist.id]) - - # create template - Playlists.save_as_template(session, playlist.id, TEMPLATE_NAME) - - # test create template - _ = Playlists.create_playlist_from_template(session, playlist, "my new name") - - # get all templates - all_templates = Playlists.get_all_templates(session) - assert len(all_templates) == 1 - # Save as template creates new playlist - assert all_templates[0] != playlist - # test delete playlist - playlist.delete(session) - - -def test_playlist_open_and_close(session): - # We need a playlist - playlist = Playlists(session, "my playlist") - - assert len(Playlists.get_open(session)) == 0 - assert len(Playlists.get_closed(session)) == 1 - - playlist.mark_open() - - assert len(Playlists.get_open(session)) == 1 - assert len(Playlists.get_closed(session)) == 0 - - playlist.close() - - assert len(Playlists.get_open(session)) == 0 - assert len(Playlists.get_closed(session)) == 1 - - -def test_playlist_get_all_and_by_id(session): - # We need two playlists - p1_name = "playlist one" - p2_name = "playlist two" - playlist1 = Playlists(session, p1_name) - _ = Playlists(session, p2_name) - - all_playlists = Playlists.get_all(session) - assert len(all_playlists) == 2 - assert p1_name in [p.name for p in all_playlists] - assert p2_name in [p.name for p in all_playlists] - assert session.get(Playlists, playlist1.id).name == p1_name - - -def test_tracks_get_all_tracks(session, track1, track2): - # Need two tracks - - result = [a.path for a in Tracks.get_all(session)] - assert track1.path in result - assert track2.path in result - - -def test_tracks_by_path(session, track1): - - assert Tracks.get_by_path(session, track1.path) is track1 - - -def test_tracks_by_id(session, track1): - - assert session.get(Tracks, track1.id) is track1 - - -def test_tracks_search_artists(session, track1): - track1_artist = "Fleetwood Mac" - - assert len(Tracks.search_artists(session, track1_artist)) == 1 - - -def test_tracks_search_titles(session, track1): - track1_title = "I'm So Afraid" - - assert len(Tracks.search_titles(session, track1_title)) == 1 - -def test_repr(session): - """Just check for error retrieving reprs""" - - nc = NoteColours(session, substring="x", colour="x") - print(nc) - - -def test_get_colour(session): - """Test for errors in execution""" - - GOOD_STRING = "cantelope" - BAD_STRING = "ericTheBee" - SUBSTR = "ant" - COLOUR = "blue" - - nc1 = NoteColours(session, substring=SUBSTR, colour=COLOUR, is_casesensitive=True) - - _ = nc1.get_colour(session, "") - colour = nc1.get_colour(session, GOOD_STRING) - assert colour == COLOUR - - colour = nc1.get_colour(session, BAD_STRING) - assert colour is None - - nc2 = NoteColours(session, substring=".*" + SUBSTR, colour=COLOUR, is_regex=True) - colour = nc2.get_colour(session, GOOD_STRING) - assert colour == COLOUR - - colour = nc2.get_colour(session, BAD_STRING) - assert colour is None - - nc3 = NoteColours( - session, substring=".*" + SUBSTR, colour=COLOUR, is_regex=True, is_casesensitive=True - ) - - colour = nc3.get_colour(session, GOOD_STRING) - assert colour == COLOUR - - colour = nc3.get_colour(session, BAD_STRING) - assert colour is None - - -def test_create_cart(session): - cart = Carts(session, 1, "name") - assert cart - print(cart) - - -def test_name_available(session): - PLAYLIST_NAME = "a name" - RENAME = "new name" - - if Playlists.name_is_available(session, PLAYLIST_NAME): - playlist = Playlists(session, PLAYLIST_NAME) - assert playlist - - assert Playlists.name_is_available(session, PLAYLIST_NAME) is False - - playlist.rename(session, RENAME) - - -def test_create_playlist_row(session): - PLAYLIST_NAME = "a name" - - if Playlists.name_is_available(session, PLAYLIST_NAME): - playlist = Playlists(session, PLAYLIST_NAME) - - plr = PlaylistRows(session, playlist.id, 1) - assert plr - print(plr) - plr.append_note("a note") - plr.append_note("another note") - - -def test_delete_plr(session): - PLAYLIST_NAME = "a name" - - if Playlists.name_is_available(session, PLAYLIST_NAME): - playlist = Playlists(session, PLAYLIST_NAME) - - plr = PlaylistRows(session, playlist.id, 1) - assert plr - PlaylistRows.delete_higher_rows(session, plr.playlist_id, 10) - - assert PlaylistRows.get_track_plr(session, 12, plr.playlist_id) is None - + plr = PlaylistRows(session, playlist.id, 1) + assert plr + PlaylistRows.delete_higher_rows(session, plr.playlist_id, 10) + assert PlaylistRows.get_track_plr(session, 12, plr.playlist_id) is None diff --git a/tests/test_playlistmodel.py b/tests/test_playlistmodel.py index 4a2ec51..2450879 100644 --- a/tests/test_playlistmodel.py +++ b/tests/test_playlistmodel.py @@ -1,380 +1,223 @@ +# Standard library imports +import os +import unittest from typing import Optional -from app.models import ( - Playlists, - Tracks, -) +# PyQt imports from PyQt6.QtCore import Qt, QModelIndex +# Third party imports +from sqlalchemy.orm.session import Session + +# App imports +from app.log import log from app.helpers import get_file_metadata -from app import playlistmodel -from dbconfig import scoped_session - -test_tracks = [ - "testdata/isa.mp3", - "testdata/isa_with_gap.mp3", - "testdata/loser.mp3", - "testdata/lovecats-10seconds.mp3", - "testdata/lovecats.mp3", - "testdata/mom.mp3", - "testdata/sitting.mp3", -] - - -def create_model_with_tracks(session: scoped_session, name: Optional[str] = None) -> "playlistmodel.PlaylistModel": - playlist = Playlists(session, name or "test playlist") - model = playlistmodel.PlaylistModel(playlist.id) - - for row in range(len(test_tracks)): - track_path = test_tracks[row % len(test_tracks)] - metadata = get_file_metadata(track_path) - track = Tracks(session, **metadata) - model.insert_row(proposed_row_number=row, track_id=track.id, note=f"{row=}") - - session.commit() - return model - - -def create_model_with_playlist_rows( - session: scoped_session, rows: int, name: Optional[str] = None -) -> "playlistmodel.PlaylistModel": - playlist = Playlists(session, name or "test playlist") - # Create a model - model = playlistmodel.PlaylistModel(playlist.id) - for row in range(rows): - model.insert_row(proposed_row_number=row, note=str(row)) - - session.commit() - return model - - -def test_11_row_playlist(monkeypatch, session): - # Create multirow playlist - monkeypatch.setattr(playlistmodel, "Session", session) - model = create_model_with_playlist_rows(session, 11) - assert model.rowCount() == 11 - assert max(model.playlist_rows.keys()) == 10 - for row in range(model.rowCount()): - assert row in model.playlist_rows - assert model.playlist_rows[row].plr_rownum == row - - -def test_move_rows_test2(monkeypatch, session): - # move row 3 to row 5 - monkeypatch.setattr(playlistmodel, "Session", session) - model = create_model_with_playlist_rows(session, 11) - model.move_rows([3], 5) - # Check we have all rows and plr_rownums are correct - for row in range(model.rowCount()): - assert row in model.playlist_rows - assert model.playlist_rows[row].plr_rownum == row - if row not in [3, 4, 5]: - assert model.playlist_rows[row].note == str(row) - elif row == 3: - assert model.playlist_rows[row].note == str(4) - elif row == 4: - assert model.playlist_rows[row].note == str(5) - elif row == 5: - assert model.playlist_rows[row].note == str(3) - - -def test_move_rows_test3(monkeypatch, session): - # move row 4 to row 3 - - monkeypatch.setattr(playlistmodel, "Session", session) - - model = create_model_with_playlist_rows(session, 11) - model.move_rows([4], 3) - - # Check we have all rows and plr_rownums are correct - for row in range(model.rowCount()): - assert row in model.playlist_rows - assert model.playlist_rows[row].plr_rownum == row - if row not in [3, 4]: - assert model.playlist_rows[row].note == str(row) - elif row == 3: - assert model.playlist_rows[row].note == str(4) - elif row == 4: - assert model.playlist_rows[row].note == str(3) - - -def test_move_rows_test4(monkeypatch, session): - # move row 4 to row 2 - - monkeypatch.setattr(playlistmodel, "Session", session) - - model = create_model_with_playlist_rows(session, 11) - model.move_rows([4], 2) - - # Check we have all rows and plr_rownums are correct - for row in range(model.rowCount()): - assert row in model.playlist_rows - assert model.playlist_rows[row].plr_rownum == row - if row not in [2, 3, 4]: - assert model.playlist_rows[row].note == str(row) - elif row == 2: - assert model.playlist_rows[row].note == str(4) - elif row == 3: - assert model.playlist_rows[row].note == str(2) - elif row == 4: - assert model.playlist_rows[row].note == str(3) - - -def test_move_rows_test5(monkeypatch, session): - # move rows [1, 4, 5, 10] → 8 - - monkeypatch.setattr(playlistmodel, "Session", session) - - model = create_model_with_playlist_rows(session, 11) - model.move_rows([1, 4, 5, 10], 8) - - # Check we have all rows and plr_rownums are correct - new_order = [] - for row in range(model.rowCount()): - assert row in model.playlist_rows - assert model.playlist_rows[row].plr_rownum == row - new_order.append(int(model.playlist_rows[row].note)) - assert new_order == [0, 2, 3, 6, 7, 8, 9, 1, 4, 5, 10] - - -def test_move_rows_test6(monkeypatch, session): - # move rows [3, 6] → 5 - - monkeypatch.setattr(playlistmodel, "Session", session) - - model = create_model_with_playlist_rows(session, 11) - model.move_rows([3, 6], 5) - - # Check we have all rows and plr_rownums are correct - new_order = [] - for row in range(model.rowCount()): - assert row in model.playlist_rows - assert model.playlist_rows[row].plr_rownum == row - new_order.append(int(model.playlist_rows[row].note)) - assert new_order == [0, 1, 2, 4, 5, 3, 6, 7, 8, 9, 10] - +# Set up test database before importing db +# Mark subsequent lines to ignore E402, imports not at top of file +DB_FILE = '/tmp/mm.db' +if os.path.exists(DB_FILE): + os.unlink(DB_FILE) +os.environ['ALCHEMICAL_DATABASE_URI'] = 'sqlite:///' + DB_FILE +from app import playlistmodel # noqa: E402 +from app.models import ( # noqa: E402 + db, + Playlists, + Settings, + Tracks, +) + + +class TestMMMisc(unittest.TestCase): + def setUp(self): + PLAYLIST_NAME = "test playlist" + self.test_tracks = [ + "testdata/isa.mp3", + "testdata/isa_with_gap.mp3", + "testdata/loser.mp3", + "testdata/lovecats-10seconds.mp3", + "testdata/lovecats.mp3", + "testdata/mom.mp3", + "testdata/sitting.mp3", + ] + + db.create_all() + + # Create a playlist and model + with db.Session() as session: + self.playlist = Playlists(session, PLAYLIST_NAME) + self.model = playlistmodel.PlaylistModel(self.playlist.id) + + for row in range(len(self.test_tracks)): + track_path = self.test_tracks[row % len(self.test_tracks)] + metadata = get_file_metadata(track_path) + track = Tracks(session, **metadata) + self.model.insert_row(proposed_row_number=row, track_id=track.id, note=f"{row=}") + + session.commit() + + def tearDown(self): + db.drop_all() + + def test_7_row_playlist(self): + # Test auto-created playlist + + assert self.model.rowCount() == 7 + assert max(self.model.playlist_rows.keys()) == 6 + for row in range(self.model.rowCount()): + assert row in self.model.playlist_rows + assert self.model.playlist_rows[row].plr_rownum == row + + +# def test_move_rows_test2(monkeypatch, session): +# # move row 3 to row 5 +# monkeypatch.setattr(playlistmodel, "Session", session) +# model = create_model_with_playlist_rows(session, 11) +# model.move_rows([3], 5) +# # Check we have all rows and plr_rownums are correct +# for row in range(model.rowCount()): +# assert row in model.playlist_rows +# assert model.playlist_rows[row].plr_rownum == row +# if row not in [3, 4, 5]: +# assert model.playlist_rows[row].note == str(row) +# elif row == 3: +# assert model.playlist_rows[row].note == str(4) +# elif row == 4: +# assert model.playlist_rows[row].note == str(5) +# elif row == 5: +# assert model.playlist_rows[row].note == str(3) + + +# def test_move_rows_test3(monkeypatch, session): +# # move row 4 to row 3 + +# monkeypatch.setattr(playlistmodel, "Session", session) + +# model = create_model_with_playlist_rows(session, 11) +# model.move_rows([4], 3) + +# # Check we have all rows and plr_rownums are correct +# for row in range(model.rowCount()): +# assert row in model.playlist_rows +# assert model.playlist_rows[row].plr_rownum == row +# if row not in [3, 4]: +# assert model.playlist_rows[row].note == str(row) +# elif row == 3: +# assert model.playlist_rows[row].note == str(4) +# elif row == 4: +# assert model.playlist_rows[row].note == str(3) + + +# def test_move_rows_test4(monkeypatch, session): +# # move row 4 to row 2 + +# monkeypatch.setattr(playlistmodel, "Session", session) + +# model = create_model_with_playlist_rows(session, 11) +# model.move_rows([4], 2) + +# # Check we have all rows and plr_rownums are correct +# for row in range(model.rowCount()): +# assert row in model.playlist_rows +# assert model.playlist_rows[row].plr_rownum == row +# if row not in [2, 3, 4]: +# assert model.playlist_rows[row].note == str(row) +# elif row == 2: +# assert model.playlist_rows[row].note == str(4) +# elif row == 3: +# assert model.playlist_rows[row].note == str(2) +# elif row == 4: +# assert model.playlist_rows[row].note == str(3) + + +# def test_move_rows_test5(monkeypatch, session): +# # move rows [1, 4, 5, 10] → 8 + +# monkeypatch.setattr(playlistmodel, "Session", session) + +# model = create_model_with_playlist_rows(session, 11) +# model.move_rows([1, 4, 5, 10], 8) -def test_move_rows_test7(monkeypatch, session): - # move rows [3, 5, 6] → 8 +# # Check we have all rows and plr_rownums are correct +# new_order = [] +# for row in range(model.rowCount()): +# assert row in model.playlist_rows +# assert model.playlist_rows[row].plr_rownum == row +# new_order.append(int(model.playlist_rows[row].note)) +# assert new_order == [0, 2, 3, 6, 7, 8, 9, 1, 4, 5, 10] - monkeypatch.setattr(playlistmodel, "Session", session) - model = create_model_with_playlist_rows(session, 11) - model.move_rows([3, 5, 6], 8) +# def test_move_rows_test6(monkeypatch, session): +# # move rows [3, 6] → 5 - # Check we have all rows and plr_rownums are correct - new_order = [] - for row in range(model.rowCount()): - assert row in model.playlist_rows - assert model.playlist_rows[row].plr_rownum == row - new_order.append(int(model.playlist_rows[row].note)) - assert new_order == [0, 1, 2, 4, 7, 8, 9, 10, 3, 5, 6] +# monkeypatch.setattr(playlistmodel, "Session", session) +# model = create_model_with_playlist_rows(session, 11) +# model.move_rows([3, 6], 5) -def test_move_rows_test8(monkeypatch, session): - # move rows [7, 8, 10] → 5 +# # Check we have all rows and plr_rownums are correct +# new_order = [] +# for row in range(model.rowCount()): +# assert row in model.playlist_rows +# assert model.playlist_rows[row].plr_rownum == row +# new_order.append(int(model.playlist_rows[row].note)) +# assert new_order == [0, 1, 2, 4, 5, 3, 6, 7, 8, 9, 10] + + +# def test_move_rows_test7(monkeypatch, session): +# # move rows [3, 5, 6] → 8 - monkeypatch.setattr(playlistmodel, "Session", session) +# monkeypatch.setattr(playlistmodel, "Session", session) - model = create_model_with_playlist_rows(session, 11) - model.move_rows([7, 8, 10], 5) +# model = create_model_with_playlist_rows(session, 11) +# model.move_rows([3, 5, 6], 8) + +# # Check we have all rows and plr_rownums are correct +# new_order = [] +# for row in range(model.rowCount()): +# assert row in model.playlist_rows +# assert model.playlist_rows[row].plr_rownum == row +# new_order.append(int(model.playlist_rows[row].note)) +# assert new_order == [0, 1, 2, 4, 7, 8, 9, 10, 3, 5, 6] - # Check we have all rows and plr_rownums are correct - new_order = [] - for row in range(model.rowCount()): - assert row in model.playlist_rows - assert model.playlist_rows[row].plr_rownum == row - new_order.append(int(model.playlist_rows[row].note)) - assert new_order == [0, 1, 2, 3, 4, 7, 8, 10, 5, 6, 9] +# def test_move_rows_test8(monkeypatch, session): +# # move rows [7, 8, 10] → 5 -def test_insert_header_row_end(monkeypatch, session): - # insert header row at end of playlist +# monkeypatch.setattr(playlistmodel, "Session", session) - monkeypatch.setattr(playlistmodel, "Session", session) - note_text = "test text" - initial_row_count = 11 +# model = create_model_with_playlist_rows(session, 11) +# model.move_rows([7, 8, 10], 5) - model = create_model_with_playlist_rows(session, initial_row_count) - model.insert_row(proposed_row_number=None, note=note_text) - assert model.rowCount() == initial_row_count + 1 - prd = model.playlist_rows[model.rowCount() - 1] - # Test against edit_role because display_role for headers is - # handled differently (sets up row span) - assert ( - model.edit_role(model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd) - == note_text - ) +# # Check we have all rows and plr_rownums are correct +# new_order = [] +# for row in range(model.rowCount()): +# assert row in model.playlist_rows +# assert model.playlist_rows[row].plr_rownum == row +# new_order.append(int(model.playlist_rows[row].note)) +# assert new_order == [0, 1, 2, 3, 4, 7, 8, 10, 5, 6, 9] -def test_insert_header_row_middle(monkeypatch, session): - # insert header row in middle of playlist +# def test_insert_header_row_end(monkeypatch, session): +# # insert header row at end of playlist - monkeypatch.setattr(playlistmodel, "Session", session) - note_text = "test text" - initial_row_count = 11 - insert_row = 6 +# monkeypatch.setattr(playlistmodel, "Session", session) +# note_text = "test text" +# initial_row_count = 11 - model = create_model_with_playlist_rows(session, initial_row_count) - model.insert_row(proposed_row_number=insert_row, note=note_text) - assert model.rowCount() == initial_row_count + 1 - prd = model.playlist_rows[insert_row] - # Test against edit_role because display_role for headers is - # handled differently (sets up row span) - assert ( - model.edit_role(model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd) - == note_text - ) +# model = create_model_with_playlist_rows(session, initial_row_count) +# model.insert_row(proposed_row_number=None, note=note_text) +# assert model.rowCount() == initial_row_count + 1 +# prd = model.playlist_rows[model.rowCount() - 1] +# # Test against edit_role because display_role for headers is +# # handled differently (sets up row span) +# assert ( +# model.edit_role(model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd) +# == note_text +# ) -def test_add_track_to_header(monkeypatch, session): - monkeypatch.setattr(playlistmodel, "Session", session) - note_text = "test text" - initial_row_count = 11 - insert_row = 6 - - model = create_model_with_playlist_rows(session, initial_row_count) - model.insert_row(proposed_row_number=insert_row, note=note_text) - assert model.rowCount() == initial_row_count + 1 - - prd = model.playlist_rows[1] - model.add_track_to_header(insert_row, prd.track_id) - - -def test_create_model_with_tracks(monkeypatch, session): - monkeypatch.setattr(playlistmodel, "Session", session) - model = create_model_with_tracks(session) - assert len(model.playlist_rows) == len(test_tracks) - - -def test_timing_one_track(monkeypatch, session): - START_ROW = 0 - END_ROW = 2 - - monkeypatch.setattr(playlistmodel, "Session", session) - model = create_model_with_tracks(session) - - model.insert_row(proposed_row_number=START_ROW, note="start+") - model.insert_row(proposed_row_number=END_ROW, note="-") - - prd = model.playlist_rows[START_ROW] - qv_value = model.display_role(START_ROW, playlistmodel.HEADER_NOTES_COLUMN, prd) - assert qv_value.value() == "start [1 tracks, 4:23 unplayed]" - - -def test_insert_track_new_playlist(monkeypatch, session): - # insert a track into a new playlist - - monkeypatch.setattr(playlistmodel, "Session", session) - - playlist = Playlists(session, "test playlist") - # Create a model - model = playlistmodel.PlaylistModel(playlist.id) - - track_path = test_tracks[0] - metadata = get_file_metadata(track_path) - track = Tracks(session, **metadata) - model.insert_row(proposed_row_number=0, track_id=track.id) - - prd = model.playlist_rows[model.rowCount() - 1] - assert ( - model.edit_role(model.rowCount() - 1, playlistmodel.Col.TITLE.value, prd) - == metadata["title"] - ) - - -def test_reverse_row_groups_one_row(monkeypatch, session): - monkeypatch.setattr(playlistmodel, "Session", session) - - rows_to_move = [3] - - model_src = create_model_with_playlist_rows(session, 5, name="source") - result = model_src._reversed_contiguous_row_groups(rows_to_move) - - assert len(result) == 1 - assert result[0] == [3] - - -def test_reverse_row_groups_multiple_row(monkeypatch, session): - monkeypatch.setattr(playlistmodel, "Session", session) - - rows_to_move = [2, 3, 4, 5, 7, 9, 10, 13, 17, 20, 21] - - model_src = create_model_with_playlist_rows(session, 5, name="source") - result = model_src._reversed_contiguous_row_groups(rows_to_move) - - assert result == [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]] - - -def test_move_one_row_between_playlists_to_end(monkeypatch, session): - monkeypatch.setattr(playlistmodel, "Session", session) - - create_rowcount = 5 - from_rows = [3] - to_row = create_rowcount - - model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") - model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination") - - model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) - model_dst.refresh_data(session) - - assert len(model_src.playlist_rows) == create_rowcount - len(from_rows) - assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows) - assert sorted([a.plr_rownum for a in model_src.playlist_rows.values()]) == list( - range(len(model_src.playlist_rows)) - ) - - -def test_move_one_row_between_playlists_to_middle(monkeypatch, session): - monkeypatch.setattr(playlistmodel, "Session", session) - - create_rowcount = 5 - from_rows = [3] - to_row = 2 - - model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") - model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination") - - model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) - model_dst.refresh_data(session) - - # Check the rows of the destination model - row_notes = [] - for row_number in range(model_dst.rowCount()): - index = model_dst.index(row_number, playlistmodel.Col.TITLE.value, QModelIndex()) - row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value()) - - assert len(model_src.playlist_rows) == create_rowcount - len(from_rows) - assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows) - assert [int(a) for a in row_notes] == [0, 1, 3, 2, 3, 4] - - -def test_move_multiple_rows_between_playlists_to_end(monkeypatch, session): - monkeypatch.setattr(playlistmodel, "Session", session) - - create_rowcount = 5 - from_rows = [1, 3, 4] - to_row = 2 - - model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") - model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination") - - model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) - model_dst.refresh_data(session) - - # Check the rows of the destination model - row_notes = [] - for row_number in range(model_dst.rowCount()): - index = model_dst.index(row_number, playlistmodel.Col.TITLE.value, QModelIndex()) - row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value()) - - assert len(model_src.playlist_rows) == create_rowcount - len(from_rows) - assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows) - assert [int(a) for a in row_notes] == [0, 1, 3, 4, 1, 2, 3, 4] - - -# def test_edit_header(monkeypatch, session): # edit header row in middle of playlist +# def test_insert_header_row_middle(monkeypatch, session): +# # insert header row in middle of playlist # monkeypatch.setattr(playlistmodel, "Session", session) # note_text = "test text" @@ -382,12 +225,188 @@ def test_move_multiple_rows_between_playlists_to_end(monkeypatch, session): # insert_row = 6 # model = create_model_with_playlist_rows(session, initial_row_count) -# model.insert_header_row(insert_row, note_text) +# model.insert_row(proposed_row_number=insert_row, note=note_text) # assert model.rowCount() == initial_row_count + 1 # prd = model.playlist_rows[insert_row] # # Test against edit_role because display_role for headers is # # handled differently (sets up row span) # assert ( -# model.edit_role(model.rowCount(), playlistmodel.Col.NOTE.value, prd) +# model.edit_role(model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd) # == note_text # ) + + +# def test_add_track_to_header(monkeypatch, session): +# monkeypatch.setattr(playlistmodel, "Session", session) +# note_text = "test text" +# initial_row_count = 11 +# insert_row = 6 + +# model = create_model_with_playlist_rows(session, initial_row_count) +# model.insert_row(proposed_row_number=insert_row, note=note_text) +# assert model.rowCount() == initial_row_count + 1 + +# prd = model.playlist_rows[1] +# model.add_track_to_header(insert_row, prd.track_id) + + +# def test_create_model_with_tracks(monkeypatch, session): +# monkeypatch.setattr(playlistmodel, "Session", session) +# model = create_model_with_tracks(session) +# assert len(model.playlist_rows) == len(self.test_tracks) + + +# def test_timing_one_track(monkeypatch, session): +# START_ROW = 0 +# END_ROW = 2 + +# monkeypatch.setattr(playlistmodel, "Session", session) +# model = create_model_with_tracks(session) + +# model.insert_row(proposed_row_number=START_ROW, note="start+") +# model.insert_row(proposed_row_number=END_ROW, note="-") + +# prd = model.playlist_rows[START_ROW] +# qv_value = model.display_role(START_ROW, playlistmodel.HEADER_NOTES_COLUMN, prd) +# assert qv_value.value() == "start [1 tracks, 4:23 unplayed]" + + +# def test_insert_track_new_playlist(monkeypatch, session): +# # insert a track into a new playlist + +# monkeypatch.setattr(playlistmodel, "Session", session) + +# playlist = Playlists(session, "test playlist") +# # Create a model +# model = playlistmodel.PlaylistModel(playlist.id) + +# track_path = self.test_tracks[0] +# metadata = get_file_metadata(track_path) +# track = Tracks(session, **metadata) +# model.insert_row(proposed_row_number=0, track_id=track.id) + +# prd = model.playlist_rows[model.rowCount() - 1] +# assert ( +# model.edit_role(model.rowCount() - 1, playlistmodel.Col.TITLE.value, prd) +# == metadata["title"] +# ) + + +# def test_reverse_row_groups_one_row(monkeypatch, session): +# monkeypatch.setattr(playlistmodel, "Session", session) + +# rows_to_move = [3] + +# model_src = create_model_with_playlist_rows(session, 5, name="source") +# result = model_src._reversed_contiguous_row_groups(rows_to_move) + +# assert len(result) == 1 +# assert result[0] == [3] + + +# def test_reverse_row_groups_multiple_row(monkeypatch, session): +# monkeypatch.setattr(playlistmodel, "Session", session) + +# rows_to_move = [2, 3, 4, 5, 7, 9, 10, 13, 17, 20, 21] + +# model_src = create_model_with_playlist_rows(session, 5, name="source") +# result = model_src._reversed_contiguous_row_groups(rows_to_move) + +# assert result == [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]] + + +# def test_move_one_row_between_playlists_to_end(monkeypatch, session): +# monkeypatch.setattr(playlistmodel, "Session", session) + +# create_rowcount = 5 +# from_rows = [3] +# to_row = create_rowcount + +# model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") +# model_dst = create_model_with_playlist_rows( +# session, create_rowcount, name="destination" +# ) + +# model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) +# model_dst.refresh_data(session) + +# assert len(model_src.playlist_rows) == create_rowcount - len(from_rows) +# assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows) +# assert sorted([a.plr_rownum for a in model_src.playlist_rows.values()]) == list( +# range(len(model_src.playlist_rows)) +# ) + + +# def test_move_one_row_between_playlists_to_middle(monkeypatch, session): +# monkeypatch.setattr(playlistmodel, "Session", session) + +# create_rowcount = 5 +# from_rows = [3] +# to_row = 2 + +# model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") +# model_dst = create_model_with_playlist_rows( +# session, create_rowcount, name="destination" +# ) + +# model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) +# model_dst.refresh_data(session) + +# # Check the rows of the destination model +# row_notes = [] +# for row_number in range(model_dst.rowCount()): +# index = model_dst.index( +# row_number, playlistmodel.Col.TITLE.value, QModelIndex() +# ) +# row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value()) + +# assert len(model_src.playlist_rows) == create_rowcount - len(from_rows) +# assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows) +# assert [int(a) for a in row_notes] == [0, 1, 3, 2, 3, 4] + + +# def test_move_multiple_rows_between_playlists_to_end(monkeypatch, session): +# monkeypatch.setattr(playlistmodel, "Session", session) + +# create_rowcount = 5 +# from_rows = [1, 3, 4] +# to_row = 2 + +# model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") +# model_dst = create_model_with_playlist_rows( +# session, create_rowcount, name="destination" +# ) + +# model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) +# model_dst.refresh_data(session) + +# # Check the rows of the destination model +# row_notes = [] +# for row_number in range(model_dst.rowCount()): +# index = model_dst.index( +# row_number, playlistmodel.Col.TITLE.value, QModelIndex() +# ) +# row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value()) + +# assert len(model_src.playlist_rows) == create_rowcount - len(from_rows) +# assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows) +# assert [int(a) for a in row_notes] == [0, 1, 3, 4, 1, 2, 3, 4] + + +# # def test_edit_header(monkeypatch, session): # edit header row in middle of playlist + +# # monkeypatch.setattr(playlistmodel, "Session", session) +# # note_text = "test text" +# # initial_row_count = 11 +# # insert_row = 6 + +# # model = create_model_with_playlist_rows(session, initial_row_count) +# # model.insert_header_row(insert_row, note_text) +# # assert model.rowCount() == initial_row_count + 1 +# # prd = model.playlist_rows[insert_row] +# # # Test against edit_role because display_role for headers is +# # # handled differently (sets up row span) +# # assert ( +# # model.edit_role(model.rowCount(), playlistmodel.Col.NOTE.value, prd) +# # == note_text +# # ) From c380d37cf9963584937360dbdbf9a81b7caf7a8d Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Fri, 5 Apr 2024 09:42:51 +0100 Subject: [PATCH 4/7] V4 WIP: mostly Black formatting --- InterceptEscapeWhenEditingTableCellInView.py | 7 +- app/dbtables.py | 4 +- app/dialogs.py | 8 +- app/models.py | 38 ++- app/pipeclient.py | 88 +++--- app/playlistmodel.py | 20 +- app/playlists.py | 1 + app/ui/main_window_ui.py | 274 +++++++++++++----- archive/audplayer.py | 42 +-- archive/spike.py | 56 ++-- archive/todo/todo.py | 26 +- ...90f8_add_played_column_to_playlist_rows.py | 16 +- ...37d3cf07f_add_playlist_dates_and_loaded.py | 12 +- .../b0983648595e_add_settings_table.py | 23 +- ...f07b96a5e60f_add_playlist_and_playtimes.py | 61 ++-- tests/test_misc.py | 4 +- tests/test_models.py | 4 +- tests/test_playlistmodel.py | 108 ++++--- 18 files changed, 496 insertions(+), 296 deletions(-) diff --git a/InterceptEscapeWhenEditingTableCellInView.py b/InterceptEscapeWhenEditingTableCellInView.py index 71bbac6..c00488f 100755 --- a/InterceptEscapeWhenEditingTableCellInView.py +++ b/InterceptEscapeWhenEditingTableCellInView.py @@ -53,7 +53,6 @@ class MyTableWidget(QTableView): class MyModel(QAbstractTableModel): - def columnCount(self, index): return 2 @@ -71,7 +70,11 @@ class MyModel(QAbstractTableModel): return QVariant() def flags(self, index): - return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEditable + return ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsEditable + ) class MainWindow(QMainWindow): diff --git a/app/dbtables.py b/app/dbtables.py index a1fe9cd..69b84e3 100644 --- a/app/dbtables.py +++ b/app/dbtables.py @@ -66,7 +66,9 @@ class PlaydatesTable(Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) lastplayed: Mapped[dt.datetime] = mapped_column(index=True) track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id")) - track: Mapped["TracksTable"] = relationship("TracksTable", back_populates="playdates") + track: Mapped["TracksTable"] = relationship( + "TracksTable", back_populates="playdates" + ) def __repr__(self) -> str: return ( diff --git a/app/dialogs.py b/app/dialogs.py index ab5465e..02b0ba2 100644 --- a/app/dialogs.py +++ b/app/dialogs.py @@ -111,7 +111,9 @@ class TrackSelectDialog(QDialog): if self.add_to_header: if move_existing and existing_prd: # "and existing_prd" for mypy's benefit - self.source_model.move_track_to_header(self.new_row_number, existing_prd, note) + self.source_model.move_track_to_header( + self.new_row_number, existing_prd, note + ) else: self.source_model.add_track_to_header(self.new_row_number, track_id) # Close dialog - we can only add one track to a header @@ -119,7 +121,9 @@ class TrackSelectDialog(QDialog): else: # Adding a new track row if move_existing and existing_prd: # "and existing_prd" for mypy's benefit - self.source_model.move_track_add_note(self.new_row_number, existing_prd, note) + self.source_model.move_track_add_note( + self.new_row_number, existing_prd, note + ) else: self.source_model.insert_row(self.new_row_number, track_id, note) diff --git a/app/models.py b/app/models.py index 0d41bd3..10f9a31 100644 --- a/app/models.py +++ b/app/models.py @@ -31,14 +31,13 @@ from log import log ALCHEMICAL_DATABASE_URI = os.environ.get("ALCHEMICAL_DATABASE_URI") if ALCHEMICAL_DATABASE_URI is None: raise ValueError("ALCHEMICAL_DATABASE_URI is undefined") -if 'unittest' in sys.modules and 'sqlite' not in ALCHEMICAL_DATABASE_URI: +if "unittest" in sys.modules and "sqlite" not in ALCHEMICAL_DATABASE_URI: raise ValueError("Unit tests running on non-Sqlite database") db = Alchemical(ALCHEMICAL_DATABASE_URI) # Database classes class Carts(dbtables.CartsTable): - def __init__( self, session: Session, @@ -61,7 +60,6 @@ class Carts(dbtables.CartsTable): class NoteColours(dbtables.NoteColoursTable): - def __init__( self, session: Session, @@ -123,7 +121,6 @@ class NoteColours(dbtables.NoteColoursTable): class Playdates(dbtables.PlaydatesTable): - def __init__(self, session: Session, track_id: int) -> None: """Record that track was played""" @@ -162,7 +159,6 @@ class Playdates(dbtables.PlaydatesTable): class Playlists(dbtables.PlaylistsTable): - def __init__(self, session: Session, name: str): self.name = name session.add(self) @@ -175,11 +171,7 @@ class Playlists(dbtables.PlaylistsTable): """ session.execute( - update(Playlists) - .where( - (Playlists.id.in_(playlist_ids)) - ) - .values(tab=None) + update(Playlists).where((Playlists.id.in_(playlist_ids))).values(tab=None) ) def close(self) -> None: @@ -295,7 +287,6 @@ class Playlists(dbtables.PlaylistsTable): class PlaylistRows(dbtables.PlaylistRowsTable): - def __init__( self, session: Session, @@ -361,9 +352,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): return session.execute(stmt).unique().scalar_one() @classmethod - def deep_rows( - cls, session: Session, playlist_id: int - ) -> Sequence["PlaylistRows"]: + def deep_rows(cls, session: Session, playlist_id: int) -> Sequence["PlaylistRows"]: """ Return a list of playlist rows that include full track and lastplayed data for given playlist_id., Sequence @@ -380,9 +369,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): return session.scalars(stmt).unique().all() @staticmethod - def delete_higher_rows( - session: Session, playlist_id: int, maxrow: int - ) -> None: + def delete_higher_rows(session: Session, playlist_id: int, maxrow: int) -> None: """ Delete rows in given playlist that have a higher row number than 'maxrow' @@ -527,10 +514,21 @@ class PlaylistRows(dbtables.PlaylistRowsTable): @classmethod def insert_row( - cls, session: Session, playlist_id: int, new_row_number: int + cls, + session: Session, + playlist_id: int, + new_row_number: int, + note: str = "", + track_id: Optional[int] = None, ) -> "PlaylistRows": cls.move_rows_down(session, playlist_id, new_row_number, 1) - return cls(session, playlist_id, new_row_number) + return cls( + session, + playlist_id=playlist_id, + row_number=new_row_number, + note=note, + track_id=track_id, + ) @staticmethod def move_rows_down( @@ -574,7 +572,6 @@ class PlaylistRows(dbtables.PlaylistRowsTable): class Settings(dbtables.SettingsTable): - def __init__(self, session: Session, name: str): self.name = name session.add(self) @@ -612,7 +609,6 @@ class Settings(dbtables.SettingsTable): class Tracks(dbtables.TracksTable): - def __repr__(self) -> str: return ( f" None: """Open _write_pipe.""" @@ -187,16 +187,16 @@ class PipeClient(): self._write_pipe.write(command + EOL) # Check that read pipe is alive if PipeClient.reader_pipe_broken.is_set(): - raise RuntimeError('PipeClient: Read-pipe error.') + raise RuntimeError("PipeClient: Read-pipe error.") try: self._write_pipe.flush() if self.timer: self._start_time = time.time() - self.reply = '' + self.reply = "" PipeClient.reply_ready.clear() except IOError as err: if err.errno == errno.EPIPE: - raise RuntimeError('PipeClient: Write-pipe error.') + raise RuntimeError("PipeClient: Write-pipe error.") else: raise @@ -211,20 +211,20 @@ class PipeClient(): line = read_pipe.readline() # Stop timer as soon as we get first line of response. stop_time = time.time() - while pipe_ok and line != '\n': + while pipe_ok and line != "\n": message += line line = read_pipe.readline() - if line == '': + if line == "": # No data in read_pipe indicates that the pipe # is broken (Audacity may have crashed). PipeClient.reader_pipe_broken.set() pipe_ok = False if self.timer: xtime = (stop_time - self._start_time) * 1000 - message += f'Execution time: {xtime:.2f}ms' + message += f"Execution time: {xtime:.2f}ms" self.reply = message PipeClient.reply_ready.set() - message = '' + message = "" def read(self) -> str: """Read Audacity's reply from pipe. @@ -238,31 +238,45 @@ class PipeClient(): """ if not PipeClient.reply_ready.is_set(): - return '' + return "" return self.reply def bool_from_string(strval) -> bool: """Return boolean value from string""" - if strval.lower() in ('true', 't', '1', 'yes', 'y'): + if strval.lower() in ("true", "t", "1", "yes", "y"): return True - if strval.lower() in ('false', 'f', '0', 'no', 'n'): + if strval.lower() in ("false", "f", "0", "no", "n"): return False - raise argparse.ArgumentTypeError('Boolean value expected.') + raise argparse.ArgumentTypeError("Boolean value expected.") def main() -> None: """Interactive command-line for PipeClient""" parser = argparse.ArgumentParser() - parser.add_argument('-t', '--timeout', type=float, metavar='', default=10, - help="timeout for reply in seconds (default: 10") - parser.add_argument('-s', '--show-time', metavar='True/False', - nargs='?', type=bool_from_string, - const='t', default='t', dest='show', - help='show command execution time (default: True)') - parser.add_argument('-d', '--docs', action='store_true', - help='show documentation and exit') + parser.add_argument( + "-t", + "--timeout", + type=float, + metavar="", + default=10, + help="timeout for reply in seconds (default: 10", + ) + parser.add_argument( + "-s", + "--show-time", + metavar="True/False", + nargs="?", + type=bool_from_string, + const="t", + default="t", + dest="show", + help="show command execution time (default: True)", + ) + parser.add_argument( + "-d", "--docs", action="store_true", help="show documentation and exit" + ) args = parser.parse_args() if args.docs: @@ -271,23 +285,23 @@ def main() -> None: client: PipeClient = PipeClient() while True: - reply: str = '' + reply: str = "" message: str = input("\nEnter command or 'Q' to quit: ") start = time.time() - if message.upper() == 'Q': + if message.upper() == "Q": sys.exit(0) - elif message == '': + elif message == "": pass else: client.write(message, timer=args.show) - while reply == '': + while reply == "": time.sleep(0.1) # allow time for reply if time.time() - start > args.timeout: - reply = 'PipeClient: Reply timed-out.' + reply = "PipeClient: Reply timed-out." else: reply = client.read() print(reply) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 8a71d5e..1e45758 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -27,6 +27,7 @@ from PyQt6.QtGui import ( # Third party imports import obsws_python as obs # type: ignore + # import snoop # type: ignore # App imports @@ -689,8 +690,9 @@ class PlaylistModel(QAbstractTableModel): < prd.plr_rownum ) ): - section_end_time = track_sequence.now.end_time + dt.timedelta( - milliseconds=duration + section_end_time = ( + track_sequence.now.end_time + + dt.timedelta(milliseconds=duration) ) end_time_str = ( ", section end time " @@ -745,7 +747,7 @@ class PlaylistModel(QAbstractTableModel): self, proposed_row_number: Optional[int], track_id: Optional[int] = None, - note: Optional[str] = None, + note: str = "", ) -> None: """ Insert a row. @@ -757,11 +759,13 @@ class PlaylistModel(QAbstractTableModel): with db.Session() as session: super().beginInsertRows(QModelIndex(), new_row_number, new_row_number) - plr = PlaylistRows.insert_row(session, self.playlist_id, new_row_number) - - plr.track_id = track_id - if note: - plr.note = note + _ = PlaylistRows.insert_row( + session=session, + playlist_id=self.playlist_id, + new_row_number=new_row_number, + note=note, + track_id=track_id, + ) self.refresh_data(session) super().endInsertRows() diff --git a/app/playlists.py b/app/playlists.py index d9d25b2..ea8791e 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -47,6 +47,7 @@ from helpers import ( from log import log from models import Settings from playlistmodel import PlaylistModel, PlaylistProxyModel + if TYPE_CHECKING: from musicmuster import Window diff --git a/app/ui/main_window_ui.py b/app/ui/main_window_ui.py index 0e2413b..99a20d2 100644 --- a/app/ui/main_window_ui.py +++ b/app/ui/main_window_ui.py @@ -15,7 +15,11 @@ class Ui_MainWindow(object): MainWindow.resize(1280, 857) MainWindow.setMinimumSize(QtCore.QSize(1280, 0)) icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(":/icons/musicmuster"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon.addPixmap( + QtGui.QPixmap(":/icons/musicmuster"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) MainWindow.setWindowIcon(icon) MainWindow.setStyleSheet("") self.centralwidget = QtWidgets.QWidget(parent=MainWindow) @@ -27,39 +31,62 @@ class Ui_MainWindow(object): self.verticalLayout_3 = QtWidgets.QVBoxLayout() self.verticalLayout_3.setObjectName("verticalLayout_3") self.previous_track_2 = QtWidgets.QLabel(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.previous_track_2.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.previous_track_2.sizePolicy().hasHeightForWidth() + ) self.previous_track_2.setSizePolicy(sizePolicy) self.previous_track_2.setMaximumSize(QtCore.QSize(230, 16777215)) font = QtGui.QFont() font.setFamily("Sans") font.setPointSize(20) self.previous_track_2.setFont(font) - self.previous_track_2.setStyleSheet("background-color: #f8d7da;\n" -"border: 1px solid rgb(85, 87, 83);") - self.previous_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.previous_track_2.setStyleSheet( + "background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);" + ) + self.previous_track_2.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignTrailing + | QtCore.Qt.AlignmentFlag.AlignVCenter + ) self.previous_track_2.setObjectName("previous_track_2") self.verticalLayout_3.addWidget(self.previous_track_2) self.current_track_2 = QtWidgets.QLabel(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.current_track_2.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.current_track_2.sizePolicy().hasHeightForWidth() + ) self.current_track_2.setSizePolicy(sizePolicy) self.current_track_2.setMaximumSize(QtCore.QSize(230, 16777215)) font = QtGui.QFont() font.setFamily("Sans") font.setPointSize(20) self.current_track_2.setFont(font) - self.current_track_2.setStyleSheet("background-color: #d4edda;\n" -"border: 1px solid rgb(85, 87, 83);") - self.current_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.current_track_2.setStyleSheet( + "background-color: #d4edda;\n" "border: 1px solid rgb(85, 87, 83);" + ) + self.current_track_2.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignTrailing + | QtCore.Qt.AlignmentFlag.AlignVCenter + ) self.current_track_2.setObjectName("current_track_2") self.verticalLayout_3.addWidget(self.current_track_2) self.next_track_2 = QtWidgets.QLabel(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.next_track_2.sizePolicy().hasHeightForWidth()) @@ -69,19 +96,29 @@ class Ui_MainWindow(object): font.setFamily("Sans") font.setPointSize(20) self.next_track_2.setFont(font) - self.next_track_2.setStyleSheet("background-color: #fff3cd;\n" -"border: 1px solid rgb(85, 87, 83);") - self.next_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.next_track_2.setStyleSheet( + "background-color: #fff3cd;\n" "border: 1px solid rgb(85, 87, 83);" + ) + self.next_track_2.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignTrailing + | QtCore.Qt.AlignmentFlag.AlignVCenter + ) self.next_track_2.setObjectName("next_track_2") self.verticalLayout_3.addWidget(self.next_track_2) self.horizontalLayout_3.addLayout(self.verticalLayout_3) self.verticalLayout = QtWidgets.QVBoxLayout() self.verticalLayout.setObjectName("verticalLayout") self.hdrPreviousTrack = QtWidgets.QLabel(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.hdrPreviousTrack.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.hdrPreviousTrack.sizePolicy().hasHeightForWidth() + ) self.hdrPreviousTrack.setSizePolicy(sizePolicy) self.hdrPreviousTrack.setMinimumSize(QtCore.QSize(0, 0)) self.hdrPreviousTrack.setMaximumSize(QtCore.QSize(16777215, 16777215)) @@ -89,32 +126,43 @@ class Ui_MainWindow(object): font.setFamily("Sans") font.setPointSize(20) self.hdrPreviousTrack.setFont(font) - self.hdrPreviousTrack.setStyleSheet("background-color: #f8d7da;\n" -"border: 1px solid rgb(85, 87, 83);") + self.hdrPreviousTrack.setStyleSheet( + "background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);" + ) self.hdrPreviousTrack.setText("") self.hdrPreviousTrack.setWordWrap(False) self.hdrPreviousTrack.setObjectName("hdrPreviousTrack") self.verticalLayout.addWidget(self.hdrPreviousTrack) self.hdrCurrentTrack = QtWidgets.QPushButton(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.hdrCurrentTrack.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.hdrCurrentTrack.sizePolicy().hasHeightForWidth() + ) self.hdrCurrentTrack.setSizePolicy(sizePolicy) font = QtGui.QFont() font.setPointSize(20) self.hdrCurrentTrack.setFont(font) - self.hdrCurrentTrack.setStyleSheet("background-color: #d4edda;\n" -"border: 1px solid rgb(85, 87, 83);\n" -"text-align: left;\n" -"padding-left: 8px;\n" -"") + self.hdrCurrentTrack.setStyleSheet( + "background-color: #d4edda;\n" + "border: 1px solid rgb(85, 87, 83);\n" + "text-align: left;\n" + "padding-left: 8px;\n" + "" + ) self.hdrCurrentTrack.setText("") self.hdrCurrentTrack.setFlat(True) self.hdrCurrentTrack.setObjectName("hdrCurrentTrack") self.verticalLayout.addWidget(self.hdrCurrentTrack) self.hdrNextTrack = QtWidgets.QPushButton(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.hdrNextTrack.sizePolicy().hasHeightForWidth()) @@ -122,10 +170,12 @@ class Ui_MainWindow(object): font = QtGui.QFont() font.setPointSize(20) self.hdrNextTrack.setFont(font) - self.hdrNextTrack.setStyleSheet("background-color: #fff3cd;\n" -"border: 1px solid rgb(85, 87, 83);\n" -"text-align: left;\n" -"padding-left: 8px;") + self.hdrNextTrack.setStyleSheet( + "background-color: #fff3cd;\n" + "border: 1px solid rgb(85, 87, 83);\n" + "text-align: left;\n" + "padding-left: 8px;" + ) self.hdrNextTrack.setText("") self.hdrNextTrack.setFlat(True) self.hdrNextTrack.setObjectName("hdrNextTrack") @@ -160,7 +210,12 @@ class Ui_MainWindow(object): self.cartsWidget.setObjectName("cartsWidget") self.horizontalLayout_Carts = QtWidgets.QHBoxLayout(self.cartsWidget) self.horizontalLayout_Carts.setObjectName("horizontalLayout_Carts") - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + spacerItem = QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum, + ) self.horizontalLayout_Carts.addItem(spacerItem) self.gridLayout_4.addWidget(self.cartsWidget, 2, 0, 1, 1) self.frame_6 = QtWidgets.QFrame(parent=self.centralwidget) @@ -205,7 +260,11 @@ class Ui_MainWindow(object): self.btnPreview = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame) self.btnPreview.setMinimumSize(QtCore.QSize(132, 41)) icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap(":/icons/headphones"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon1.addPixmap( + QtGui.QPixmap(":/icons/headphones"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.btnPreview.setIcon(icon1) self.btnPreview.setIconSize(QtCore.QSize(30, 30)) self.btnPreview.setCheckable(True) @@ -289,10 +348,15 @@ class Ui_MainWindow(object): self.label_silent_timer.setObjectName("label_silent_timer") self.horizontalLayout.addWidget(self.frame_silent) self.widgetFadeVolume = PlotWidget(parent=self.InfoFooterFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widgetFadeVolume.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.widgetFadeVolume.sizePolicy().hasHeightForWidth() + ) self.widgetFadeVolume.setSizePolicy(sizePolicy) self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0)) self.widgetFadeVolume.setObjectName("widgetFadeVolume") @@ -309,7 +373,11 @@ class Ui_MainWindow(object): self.btnFade.setMinimumSize(QtCore.QSize(132, 32)) self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215)) icon2 = QtGui.QIcon() - icon2.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon2.addPixmap( + QtGui.QPixmap(":/icons/fade"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.btnFade.setIcon(icon2) self.btnFade.setIconSize(QtCore.QSize(30, 30)) self.btnFade.setObjectName("btnFade") @@ -317,7 +385,11 @@ class Ui_MainWindow(object): self.btnStop = QtWidgets.QPushButton(parent=self.frame) self.btnStop.setMinimumSize(QtCore.QSize(0, 36)) icon3 = QtGui.QIcon() - icon3.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon3.addPixmap( + QtGui.QPixmap(":/icons/stopsign"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.btnStop.setIcon(icon3) self.btnStop.setObjectName("btnStop") self.verticalLayout_5.addWidget(self.btnStop) @@ -343,39 +415,69 @@ class Ui_MainWindow(object): MainWindow.setStatusBar(self.statusbar) self.actionPlay_next = QtGui.QAction(parent=MainWindow) icon4 = QtGui.QIcon() - icon4.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon4.addPixmap( + QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-play.png"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.actionPlay_next.setIcon(icon4) self.actionPlay_next.setObjectName("actionPlay_next") self.actionSkipToNext = QtGui.QAction(parent=MainWindow) icon5 = QtGui.QIcon() - icon5.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon5.addPixmap( + QtGui.QPixmap(":/icons/next"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.actionSkipToNext.setIcon(icon5) self.actionSkipToNext.setObjectName("actionSkipToNext") self.actionInsertTrack = QtGui.QAction(parent=MainWindow) icon6 = QtGui.QIcon() - icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon6.addPixmap( + QtGui.QPixmap( + "app/ui/../../../../.designer/backup/icon_search_database.png" + ), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.actionInsertTrack.setIcon(icon6) self.actionInsertTrack.setObjectName("actionInsertTrack") self.actionAdd_file = QtGui.QAction(parent=MainWindow) icon7 = QtGui.QIcon() - icon7.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon7.addPixmap( + QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_open_file.png"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.actionAdd_file.setIcon(icon7) self.actionAdd_file.setObjectName("actionAdd_file") self.actionFade = QtGui.QAction(parent=MainWindow) icon8 = QtGui.QIcon() - icon8.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon8.addPixmap( + QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-fade.png"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.actionFade.setIcon(icon8) self.actionFade.setObjectName("actionFade") self.actionStop = QtGui.QAction(parent=MainWindow) icon9 = QtGui.QIcon() - icon9.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon9.addPixmap( + QtGui.QPixmap(":/icons/stop"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.actionStop.setIcon(icon9) self.actionStop.setObjectName("actionStop") self.action_Clear_selection = QtGui.QAction(parent=MainWindow) self.action_Clear_selection.setObjectName("action_Clear_selection") self.action_Resume_previous = QtGui.QAction(parent=MainWindow) icon10 = QtGui.QIcon() - icon10.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon10.addPixmap( + QtGui.QPixmap(":/icons/previous"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.action_Resume_previous.setIcon(icon10) self.action_Resume_previous.setObjectName("action_Resume_previous") self.actionE_xit = QtGui.QAction(parent=MainWindow) @@ -422,7 +524,9 @@ class Ui_MainWindow(object): self.actionImport = QtGui.QAction(parent=MainWindow) self.actionImport.setObjectName("actionImport") self.actionDownload_CSV_of_played_tracks = QtGui.QAction(parent=MainWindow) - self.actionDownload_CSV_of_played_tracks.setObjectName("actionDownload_CSV_of_played_tracks") + self.actionDownload_CSV_of_played_tracks.setObjectName( + "actionDownload_CSV_of_played_tracks" + ) self.actionSearch = QtGui.QAction(parent=MainWindow) self.actionSearch.setObjectName("actionSearch") self.actionInsertSectionHeader = QtGui.QAction(parent=MainWindow) @@ -450,9 +554,13 @@ class Ui_MainWindow(object): self.actionResume = QtGui.QAction(parent=MainWindow) self.actionResume.setObjectName("actionResume") self.actionSearch_title_in_Wikipedia = QtGui.QAction(parent=MainWindow) - self.actionSearch_title_in_Wikipedia.setObjectName("actionSearch_title_in_Wikipedia") + self.actionSearch_title_in_Wikipedia.setObjectName( + "actionSearch_title_in_Wikipedia" + ) self.actionSearch_title_in_Songfacts = QtGui.QAction(parent=MainWindow) - self.actionSearch_title_in_Songfacts.setObjectName("actionSearch_title_in_Songfacts") + self.actionSearch_title_in_Songfacts.setObjectName( + "actionSearch_title_in_Songfacts" + ) self.actionSelect_duplicate_rows = QtGui.QAction(parent=MainWindow) self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows") self.menuFile.addAction(self.actionNewPlaylist) @@ -503,7 +611,7 @@ class Ui_MainWindow(object): self.retranslateUi(MainWindow) self.tabPlaylist.setCurrentIndex(-1) self.tabInfolist.setCurrentIndex(-1) - self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore + self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): @@ -539,38 +647,58 @@ class Ui_MainWindow(object): self.actionFade.setShortcut(_translate("MainWindow", "Ctrl+Z")) self.actionStop.setText(_translate("MainWindow", "S&top")) self.actionStop.setShortcut(_translate("MainWindow", "Ctrl+Alt+S")) - self.action_Clear_selection.setText(_translate("MainWindow", "Clear &selection")) + self.action_Clear_selection.setText( + _translate("MainWindow", "Clear &selection") + ) self.action_Clear_selection.setShortcut(_translate("MainWindow", "Esc")) - self.action_Resume_previous.setText(_translate("MainWindow", "&Resume previous")) + self.action_Resume_previous.setText( + _translate("MainWindow", "&Resume previous") + ) self.actionE_xit.setText(_translate("MainWindow", "E&xit")) self.actionTest.setText(_translate("MainWindow", "&Test")) self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen...")) self.actionNewPlaylist.setText(_translate("MainWindow", "&New...")) self.actionTestFunction.setText(_translate("MainWindow", "&Test function")) - self.actionSkipToFade.setText(_translate("MainWindow", "&Skip to start of fade")) + self.actionSkipToFade.setText( + _translate("MainWindow", "&Skip to start of fade") + ) self.actionSkipToEnd.setText(_translate("MainWindow", "Skip to &end of track")) self.actionClosePlaylist.setText(_translate("MainWindow", "&Close")) self.actionRenamePlaylist.setText(_translate("MainWindow", "&Rename...")) self.actionDeletePlaylist.setText(_translate("MainWindow", "Dele&te...")) - self.actionMoveSelected.setText(_translate("MainWindow", "Mo&ve selected tracks to...")) + self.actionMoveSelected.setText( + _translate("MainWindow", "Mo&ve selected tracks to...") + ) self.actionExport_playlist.setText(_translate("MainWindow", "E&xport...")) self.actionSetNext.setText(_translate("MainWindow", "Set &next")) self.actionSetNext.setShortcut(_translate("MainWindow", "Ctrl+N")) - self.actionSelect_next_track.setText(_translate("MainWindow", "Select next track")) + self.actionSelect_next_track.setText( + _translate("MainWindow", "Select next track") + ) self.actionSelect_next_track.setShortcut(_translate("MainWindow", "J")) - self.actionSelect_previous_track.setText(_translate("MainWindow", "Select previous track")) + self.actionSelect_previous_track.setText( + _translate("MainWindow", "Select previous track") + ) self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K")) - self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks")) - self.actionMoveUnplayed.setText(_translate("MainWindow", "Move &unplayed tracks to...")) + self.actionSelect_played_tracks.setText( + _translate("MainWindow", "Select played tracks") + ) + self.actionMoveUnplayed.setText( + _translate("MainWindow", "Move &unplayed tracks to...") + ) self.actionAdd_note.setText(_translate("MainWindow", "Add note...")) self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T")) self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls")) self.actionImport.setText(_translate("MainWindow", "Import track...")) self.actionImport.setShortcut(_translate("MainWindow", "Ctrl+Shift+I")) - self.actionDownload_CSV_of_played_tracks.setText(_translate("MainWindow", "Download CSV of played tracks...")) + self.actionDownload_CSV_of_played_tracks.setText( + _translate("MainWindow", "Download CSV of played tracks...") + ) self.actionSearch.setText(_translate("MainWindow", "Search...")) self.actionSearch.setShortcut(_translate("MainWindow", "/")) - self.actionInsertSectionHeader.setText(_translate("MainWindow", "Insert §ion header...")) + self.actionInsertSectionHeader.setText( + _translate("MainWindow", "Insert §ion header...") + ) self.actionInsertSectionHeader.setShortcut(_translate("MainWindow", "Ctrl+H")) self.actionRemove.setText(_translate("MainWindow", "&Remove track")) self.actionFind_next.setText(_translate("MainWindow", "Find next")) @@ -578,8 +706,12 @@ class Ui_MainWindow(object): self.actionFind_previous.setText(_translate("MainWindow", "Find previous")) self.actionFind_previous.setShortcut(_translate("MainWindow", "P")) self.action_About.setText(_translate("MainWindow", "&About")) - self.actionSave_as_template.setText(_translate("MainWindow", "Save as template...")) - self.actionNew_from_template.setText(_translate("MainWindow", "New from template...")) + self.actionSave_as_template.setText( + _translate("MainWindow", "Save as template...") + ) + self.actionNew_from_template.setText( + _translate("MainWindow", "New from template...") + ) self.actionDebug.setText(_translate("MainWindow", "Debug")) self.actionAdd_cart.setText(_translate("MainWindow", "Edit cart &1...")) self.actionMark_for_moving.setText(_translate("MainWindow", "Mark for moving")) @@ -588,10 +720,22 @@ class Ui_MainWindow(object): self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V")) self.actionResume.setText(_translate("MainWindow", "Resume")) self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R")) - self.actionSearch_title_in_Wikipedia.setText(_translate("MainWindow", "Search title in Wikipedia")) - self.actionSearch_title_in_Wikipedia.setShortcut(_translate("MainWindow", "Ctrl+W")) - self.actionSearch_title_in_Songfacts.setText(_translate("MainWindow", "Search title in Songfacts")) - self.actionSearch_title_in_Songfacts.setShortcut(_translate("MainWindow", "Ctrl+S")) - self.actionSelect_duplicate_rows.setText(_translate("MainWindow", "Select duplicate rows...")) + self.actionSearch_title_in_Wikipedia.setText( + _translate("MainWindow", "Search title in Wikipedia") + ) + self.actionSearch_title_in_Wikipedia.setShortcut( + _translate("MainWindow", "Ctrl+W") + ) + self.actionSearch_title_in_Songfacts.setText( + _translate("MainWindow", "Search title in Songfacts") + ) + self.actionSearch_title_in_Songfacts.setShortcut( + _translate("MainWindow", "Ctrl+S") + ) + self.actionSelect_duplicate_rows.setText( + _translate("MainWindow", "Select duplicate rows...") + ) + + from infotabs import InfoTabs from pyqtgraph import PlotWidget diff --git a/archive/audplayer.py b/archive/audplayer.py index d9442ef..7d4074e 100755 --- a/archive/audplayer.py +++ b/archive/audplayer.py @@ -49,9 +49,9 @@ def leading_silence(audio_segment, silence_threshold=-50.0, chunk_size=10): trim_ms = 0 # ms assert chunk_size > 0 # to avoid infinite loop - while ( - audio_segment[trim_ms:trim_ms + chunk_size].dBFS < silence_threshold - and trim_ms < len(audio_segment)): + while audio_segment[ + trim_ms : trim_ms + chunk_size + ].dBFS < silence_threshold and trim_ms < len(audio_segment): trim_ms += chunk_size # if there is no end it should return the length of the segment @@ -72,8 +72,9 @@ def significant_fade(audio_segment, fade_threshold=-20.0, chunk_size=10): segment_length = audio_segment.duration_seconds * 1000 # ms trim_ms = segment_length - chunk_size while ( - audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold - and trim_ms > 0): + audio_segment[trim_ms : trim_ms + chunk_size].dBFS < fade_threshold + and trim_ms > 0 + ): trim_ms -= chunk_size # if there is no trailing silence, return lenght of track (it's less @@ -94,8 +95,9 @@ def trailing_silence(audio_segment, silence_threshold=-50.0, chunk_size=10): segment_length = audio_segment.duration_seconds * 1000 # ms trim_ms = segment_length - chunk_size while ( - audio_segment[trim_ms:trim_ms + chunk_size].dBFS < silence_threshold - and trim_ms > 0): + audio_segment[trim_ms : trim_ms + chunk_size].dBFS < silence_threshold + and trim_ms > 0 + ): trim_ms -= chunk_size # if there is no trailing silence, return lenght of track (it's less @@ -124,15 +126,17 @@ def update_progress(player, talk_at, silent_at): remaining_time = total_time - elapsed_time talk_time = remaining_time - (total_time - talk_at) silent_time = remaining_time - (total_time - silent_at) - end_time = (dt.datetime.now() + timedelta( - milliseconds=remaining_time)).strftime("%H:%M:%S") + end_time = (dt.datetime.now() + timedelta(milliseconds=remaining_time)).strftime( + "%H:%M:%S" + ) print( f"\t{ms_to_mmss(elapsed_time)}/" f"{ms_to_mmss(total_time)}\t\t" f"Talk in: {ms_to_mmss(talk_time)} " f"Silent in: {ms_to_mmss(silent_time)} " - f"Ends at: {end_time} [{ms_to_mmss(remaining_time)}]" - , end="\r") + f"Ends at: {end_time} [{ms_to_mmss(remaining_time)}]", + end="\r", + ) # Print name of current song, print name of next song. Play current when @@ -163,21 +167,21 @@ def test(): test() # next_song = get_next_song -# +# # def play_track(): # r = run_aud_cmd("--current-song-length -# -# -# +# +# +# # def play(): # play_track() # songtimer_start() -# -# +# +# # print("Start playing in 3 seconds") -# +# # sleep(3) -# +# # play() diff --git a/archive/spike.py b/archive/spike.py index 5757359..b4b6ea6 100644 --- a/archive/spike.py +++ b/archive/spike.py @@ -1,5 +1,3 @@ - - # tl = Timeloop() # # @@ -48,34 +46,34 @@ # rt.stop() # better in a try/finally block to make sure the program ends! # print("End") - #def kae2(self, index): - # print(f"table header click, index={index}") +# def kae2(self, index): +# print(f"table header click, index={index}") - #def kae(self, a, b, c): - # self.data.append(f"a={a}, b={b}, c={c}") +# def kae(self, a, b, c): +# self.data.append(f"a={a}, b={b}, c={c}") - #def mousePressEvent(self, QMouseEvent): - # print("mouse press") +# def mousePressEvent(self, QMouseEvent): +# print("mouse press") - #def mouseReleaseEvent(self, QMouseEvent): - # print("mouse release") - # # QMessageBox.about( - # # self, - # # "About Sample Editor", - # # "\n".join(self.data) - # # ) - #def eventFilter(self, obj, event): - # # you could be doing different groups of actions - # # for different types of widgets and either filtering - # # the event or not. - # # Here we just check if its one of the layout widgets - # # if self.layout.indexOf(obj) != -1: - # # print(f"event received: {event.type()}") - # if event.type() == QEvent.MouseButtonPress: - # print("Widget click") - # # if I returned True right here, the event - # # would be filtered and not reach the obj, - # # meaning that I decided to handle it myself +# def mouseReleaseEvent(self, QMouseEvent): +# print("mouse release") +# # QMessageBox.about( +# # self, +# # "About Sample Editor", +# # "\n".join(self.data) +# # ) +# def eventFilter(self, obj, event): +# # you could be doing different groups of actions +# # for different types of widgets and either filtering +# # the event or not. +# # Here we just check if its one of the layout widgets +# # if self.layout.indexOf(obj) != -1: +# # print(f"event received: {event.type()}") +# if event.type() == QEvent.MouseButtonPress: +# print("Widget click") +# # if I returned True right here, the event +# # would be filtered and not reach the obj, +# # meaning that I decided to handle it myself - # # regardless, just do the default - # return super().eventFilter(obj, event) +# # regardless, just do the default +# return super().eventFilter(obj, event) diff --git a/archive/todo/todo.py b/archive/todo/todo.py index eb1aa3b..ac46aad 100644 --- a/archive/todo/todo.py +++ b/archive/todo/todo.py @@ -7,19 +7,19 @@ from PyQt5.QtCore import Qt qt_creator_file = "mainwindow.ui" Ui_MainWindow, QtBaseClass = uic.loadUiType(qt_creator_file) -tick = QtGui.QImage('tick.png') +tick = QtGui.QImage("tick.png") class TodoModel(QtCore.QAbstractListModel): def __init__(self, *args, todos=None, **kwargs): super(TodoModel, self).__init__(*args, **kwargs) self.todos = todos or [] - + def data(self, index, role): if role == Qt.DisplayRole: _, text = self.todos[index.row()] return text - + if role == Qt.DecorationRole: status, _ = self.todos[index.row()] if status: @@ -51,15 +51,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): and then clearing it. """ text = self.todoEdit.text() - if text: # Don't add empty strings. + if text: # Don't add empty strings. # Access the list via the model. self.model.todos.append((False, text)) - # Trigger refresh. + # Trigger refresh. self.model.layoutChanged.emit() - # Empty the input + # Empty the input self.todoEdit.setText("") self.save() - + def delete(self): indexes = self.todoView.selectedIndexes() if indexes: @@ -71,7 +71,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): # Clear the selection (as it is no longer valid). self.todoView.clearSelection() self.save() - + def complete(self): indexes = self.todoView.selectedIndexes() if indexes: @@ -79,22 +79,22 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): row = index.row() status, text = self.model.todos[row] self.model.todos[row] = (True, text) - # .dataChanged takes top-left and bottom right, which are equal + # .dataChanged takes top-left and bottom right, which are equal # for a single selection. self.model.dataChanged.emit(index, index) # Clear the selection (as it is no longer valid). self.todoView.clearSelection() self.save() - + def load(self): try: - with open('data.db', 'r') as f: + with open("data.db", "r") as f: self.model.todos = json.load(f) except Exception: pass def save(self): - with open('data.db', 'w') as f: + with open("data.db", "w") as f: data = json.dump(self.model.todos, f) @@ -102,5 +102,3 @@ app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() app.exec_() - - diff --git a/migrations/versions/0c604bf490f8_add_played_column_to_playlist_rows.py b/migrations/versions/0c604bf490f8_add_played_column_to_playlist_rows.py index 72b9a6c..975ce92 100644 --- a/migrations/versions/0c604bf490f8_add_played_column_to_playlist_rows.py +++ b/migrations/versions/0c604bf490f8_add_played_column_to_playlist_rows.py @@ -10,23 +10,23 @@ import sqlalchemy as sa from sqlalchemy.dialects import mysql # revision identifiers, used by Alembic. -revision = '0c604bf490f8' -down_revision = '29c0d7ffc741' +revision = "0c604bf490f8" +down_revision = "29c0d7ffc741" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('playlist_rows', sa.Column('played', sa.Boolean(), nullable=False)) - op.drop_index('ix_tracks_lastplayed', table_name='tracks') - op.drop_column('tracks', 'lastplayed') + op.add_column("playlist_rows", sa.Column("played", sa.Boolean(), nullable=False)) + op.drop_index("ix_tracks_lastplayed", table_name="tracks") + op.drop_column("tracks", "lastplayed") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('tracks', sa.Column('lastplayed', mysql.DATETIME(), nullable=True)) - op.create_index('ix_tracks_lastplayed', 'tracks', ['lastplayed'], unique=False) - op.drop_column('playlist_rows', 'played') + op.add_column("tracks", sa.Column("lastplayed", mysql.DATETIME(), nullable=True)) + op.create_index("ix_tracks_lastplayed", "tracks", ["lastplayed"], unique=False) + op.drop_column("playlist_rows", "played") # ### end Alembic commands ### diff --git a/migrations/versions/2cc37d3cf07f_add_playlist_dates_and_loaded.py b/migrations/versions/2cc37d3cf07f_add_playlist_dates_and_loaded.py index 6978768..5a39927 100644 --- a/migrations/versions/2cc37d3cf07f_add_playlist_dates_and_loaded.py +++ b/migrations/versions/2cc37d3cf07f_add_playlist_dates_and_loaded.py @@ -10,21 +10,21 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '2cc37d3cf07f' -down_revision = 'e3b04db5506f' +revision = "2cc37d3cf07f" +down_revision = "e3b04db5506f" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('playlists', sa.Column('last_used', sa.DateTime(), nullable=True)) - op.add_column('playlists', sa.Column('loaded', sa.Boolean(), nullable=True)) + op.add_column("playlists", sa.Column("last_used", sa.DateTime(), nullable=True)) + op.add_column("playlists", sa.Column("loaded", sa.Boolean(), nullable=True)) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('playlists', 'loaded') - op.drop_column('playlists', 'last_used') + op.drop_column("playlists", "loaded") + op.drop_column("playlists", "last_used") # ### end Alembic commands ### diff --git a/migrations/versions/b0983648595e_add_settings_table.py b/migrations/versions/b0983648595e_add_settings_table.py index 8820529..34ef976 100644 --- a/migrations/versions/b0983648595e_add_settings_table.py +++ b/migrations/versions/b0983648595e_add_settings_table.py @@ -10,27 +10,28 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'b0983648595e' -down_revision = '1bc727e5e87f' +revision = "b0983648595e" +down_revision = "1bc727e5e87f" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('settings', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('name', sa.String(length=32), nullable=False), - sa.Column('f_datetime', sa.DateTime(), nullable=True), - sa.Column('f_int', sa.Integer(), nullable=True), - sa.Column('f_string', sa.String(length=128), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name') + op.create_table( + "settings", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("name", sa.String(length=32), nullable=False), + sa.Column("f_datetime", sa.DateTime(), nullable=True), + sa.Column("f_int", sa.Integer(), nullable=True), + sa.Column("f_string", sa.String(length=128), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('settings') + op.drop_table("settings") # ### end Alembic commands ### diff --git a/migrations/versions/f07b96a5e60f_add_playlist_and_playtimes.py b/migrations/versions/f07b96a5e60f_add_playlist_and_playtimes.py index 5649a14..e3c3238 100644 --- a/migrations/versions/f07b96a5e60f_add_playlist_and_playtimes.py +++ b/migrations/versions/f07b96a5e60f_add_playlist_and_playtimes.py @@ -10,43 +10,54 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'f07b96a5e60f' -down_revision = 'b0983648595e' +revision = "f07b96a5e60f" +down_revision = "b0983648595e" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('playdates', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('lastplayed', sa.DateTime(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + "playdates", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("lastplayed", sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_playdates_lastplayed'), 'playdates', ['lastplayed'], unique=False) - op.create_table('playlists', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('name', sa.String(length=32), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name') + op.create_index( + op.f("ix_playdates_lastplayed"), "playdates", ["lastplayed"], unique=False ) - op.create_table('playlistracks', - sa.Column('playlist_id', sa.Integer(), nullable=True), - sa.Column('track_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['playlist_id'], ['playlists.id'], ), - sa.ForeignKeyConstraint(['track_id'], ['tracks.id'], ) + op.create_table( + "playlists", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("name", sa.String(length=32), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), ) - op.add_column('tracks', sa.Column('playdates_id', sa.Integer(), nullable=True)) - op.create_foreign_key(None, 'tracks', 'playdates', ['playdates_id'], ['id']) + op.create_table( + "playlistracks", + sa.Column("playlist_id", sa.Integer(), nullable=True), + sa.Column("track_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["playlist_id"], + ["playlists.id"], + ), + sa.ForeignKeyConstraint( + ["track_id"], + ["tracks.id"], + ), + ) + op.add_column("tracks", sa.Column("playdates_id", sa.Integer(), nullable=True)) + op.create_foreign_key(None, "tracks", "playdates", ["playdates_id"], ["id"]) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'tracks', type_='foreignkey') - op.drop_column('tracks', 'playdates_id') - op.drop_table('playlistracks') - op.drop_table('playlists') - op.drop_index(op.f('ix_playdates_lastplayed'), table_name='playdates') - op.drop_table('playdates') + op.drop_constraint(None, "tracks", type_="foreignkey") + op.drop_column("tracks", "playdates_id") + op.drop_table("playlistracks") + op.drop_table("playlists") + op.drop_index(op.f("ix_playdates_lastplayed"), table_name="playdates") + op.drop_table("playdates") # ### end Alembic commands ### diff --git a/tests/test_misc.py b/tests/test_misc.py index 4fdff4d..de26971 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -12,10 +12,10 @@ import pytest # Mark subsequent lines to ignore E402, imports not at top of file # Set up test database before importing db # Mark subsequent lines to ignore E402, imports not at top of file -DB_FILE = '/tmp/mm.db' +DB_FILE = "/tmp/mm.db" if os.path.exists(DB_FILE): os.unlink(DB_FILE) -os.environ['ALCHEMICAL_DATABASE_URI'] = 'sqlite:///' + DB_FILE +os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE from models import db, Settings # noqa: E402 diff --git a/tests/test_models.py b/tests/test_models.py index 7c4dc3b..bbad41c 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -12,10 +12,10 @@ from app import helpers # Set up test database before importing db # Mark subsequent lines to ignore E402, imports not at top of file -DB_FILE = '/tmp/mm.db' +DB_FILE = "/tmp/mm.db" if os.path.exists(DB_FILE): os.unlink(DB_FILE) -os.environ['ALCHEMICAL_DATABASE_URI'] = 'sqlite:///' + DB_FILE +os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE from app.models import ( # noqa: E402 db, Carts, diff --git a/tests/test_playlistmodel.py b/tests/test_playlistmodel.py index 2450879..281e4c0 100644 --- a/tests/test_playlistmodel.py +++ b/tests/test_playlistmodel.py @@ -12,12 +12,13 @@ from sqlalchemy.orm.session import Session # App imports from app.log import log from app.helpers import get_file_metadata + # Set up test database before importing db # Mark subsequent lines to ignore E402, imports not at top of file -DB_FILE = '/tmp/mm.db' +DB_FILE = "/tmp/mm.db" if os.path.exists(DB_FILE): os.unlink(DB_FILE) -os.environ['ALCHEMICAL_DATABASE_URI'] = 'sqlite:///' + DB_FILE +os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE from app import playlistmodel # noqa: E402 from app.models import ( # noqa: E402 db, @@ -27,64 +28,83 @@ from app.models import ( # noqa: E402 ) -class TestMMMisc(unittest.TestCase): +# class TestMMMiscTracks(unittest.TestCase): +# def setUp(self): +# PLAYLIST_NAME = "tracks playlist" +# self.test_tracks = [ +# "testdata/isa.mp3", +# "testdata/isa_with_gap.mp3", +# "testdata/loser.mp3", +# "testdata/lovecats-10seconds.mp3", +# "testdata/lovecats.mp3", +# "testdata/mom.mp3", +# "testdata/sitting.mp3", +# ] + +# db.create_all() + +# # Create a playlist and model +# with db.Session() as session: +# self.playlist = Playlists(session, PLAYLIST_NAME) +# self.model = playlistmodel.PlaylistModel(self.playlist.id) + +# for row in range(len(self.test_tracks)): +# track_path = self.test_tracks[row % len(self.test_tracks)] +# metadata = get_file_metadata(track_path) +# track = Tracks(session, **metadata) +# self.model.insert_row( +# proposed_row_number=row, track_id=track.id, note=f"{row=}" +# ) + +# session.commit() + +# def tearDown(self): +# db.drop_all() + +# def test_7_row_playlist(self): +# # Test auto-created playlist + +# assert self.model.rowCount() == 7 +# assert max(self.model.playlist_rows.keys()) == 6 +# for row in range(self.model.rowCount()): +# assert row in self.model.playlist_rows +# assert self.model.playlist_rows[row].plr_rownum == row + + +class TestMMMiscRowMove(unittest.TestCase): def setUp(self): - PLAYLIST_NAME = "test playlist" - self.test_tracks = [ - "testdata/isa.mp3", - "testdata/isa_with_gap.mp3", - "testdata/loser.mp3", - "testdata/lovecats-10seconds.mp3", - "testdata/lovecats.mp3", - "testdata/mom.mp3", - "testdata/sitting.mp3", - ] + PLAYLIST_NAME = "rowmove playlist" + ROWS_TO_CREATE = 11 db.create_all() - # Create a playlist and model with db.Session() as session: self.playlist = Playlists(session, PLAYLIST_NAME) self.model = playlistmodel.PlaylistModel(self.playlist.id) - - for row in range(len(self.test_tracks)): - track_path = self.test_tracks[row % len(self.test_tracks)] - metadata = get_file_metadata(track_path) - track = Tracks(session, **metadata) - self.model.insert_row(proposed_row_number=row, track_id=track.id, note=f"{row=}") + for row in range(ROWS_TO_CREATE): + print(f"{row=}") + self.model.insert_row(proposed_row_number=row, note=str(row)) session.commit() def tearDown(self): db.drop_all() - def test_7_row_playlist(self): - # Test auto-created playlist - - assert self.model.rowCount() == 7 - assert max(self.model.playlist_rows.keys()) == 6 + def test_move_rows_test2(self): + # move row 3 to row 5 + self.model.move_rows([3], 5) + # Check we have all rows and plr_rownums are correct for row in range(self.model.rowCount()): assert row in self.model.playlist_rows assert self.model.playlist_rows[row].plr_rownum == row - - -# def test_move_rows_test2(monkeypatch, session): -# # move row 3 to row 5 -# monkeypatch.setattr(playlistmodel, "Session", session) -# model = create_model_with_playlist_rows(session, 11) -# model.move_rows([3], 5) -# # Check we have all rows and plr_rownums are correct -# for row in range(model.rowCount()): -# assert row in model.playlist_rows -# assert model.playlist_rows[row].plr_rownum == row -# if row not in [3, 4, 5]: -# assert model.playlist_rows[row].note == str(row) -# elif row == 3: -# assert model.playlist_rows[row].note == str(4) -# elif row == 4: -# assert model.playlist_rows[row].note == str(5) -# elif row == 5: -# assert model.playlist_rows[row].note == str(3) + if row not in [3, 4, 5]: + assert self.model.playlist_rows[row].note == str(row) + elif row == 3: + assert self.model.playlist_rows[row].note == str(4) + elif row == 4: + assert self.model.playlist_rows[row].note == str(5) + elif row == 5: + assert self.model.playlist_rows[row].note == str(3) # def test_move_rows_test3(monkeypatch, session): From 92d85304f2da51b2e7ae7bb22d59d2b4dd437276 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Fri, 5 Apr 2024 14:42:04 +0100 Subject: [PATCH 5/7] Put commit()s where needed, move some info to debug logging --- app/models.py | 14 ++++---- app/playlistmodel.py | 8 ++--- tests/test_playlistmodel.py | 69 ++++++++++++++++++------------------- 3 files changed, 45 insertions(+), 46 deletions(-) diff --git a/app/models.py b/app/models.py index 10f9a31..e0c8f0f 100644 --- a/app/models.py +++ b/app/models.py @@ -78,7 +78,7 @@ class NoteColours(dbtables.NoteColoursTable): self.order = order session.add(self) - session.flush() + session.commit() @classmethod def get_all(cls, session: Session) -> Sequence["NoteColours"]: @@ -162,7 +162,7 @@ class Playlists(dbtables.PlaylistsTable): def __init__(self, session: Session, name: str): self.name = name session.add(self) - session.flush() + session.commit() @staticmethod def clear_tabs(session: Session, playlist_ids: List[int]) -> None: @@ -201,7 +201,7 @@ class Playlists(dbtables.PlaylistsTable): """ self.deleted = True - session.flush() + session.commit() @classmethod def get_all(cls, session: Session) -> Sequence["Playlists"]: @@ -268,7 +268,7 @@ class Playlists(dbtables.PlaylistsTable): """ self.name = new_name - session.flush() + session.commit() @staticmethod def save_as_template( @@ -381,7 +381,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): PlaylistRows.plr_rownum > maxrow, ) ) - session.flush() + session.commit() @staticmethod def delete_row(session: Session, playlist_id: int, row_number: int) -> None: @@ -575,7 +575,7 @@ class Settings(dbtables.SettingsTable): def __init__(self, session: Session, name: str): self.name = name session.add(self) - session.flush() + session.commit() @classmethod def all_as_dict(cls, session): @@ -605,7 +605,7 @@ class Settings(dbtables.SettingsTable): for key, value in data.items(): assert hasattr(self, key) setattr(self, key, value) - session.flush() + session.commit() class Tracks(dbtables.TracksTable): diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 1e45758..66b2477 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -123,7 +123,7 @@ class PlaylistModel(QAbstractTableModel): *args, **kwargs, ): - log.info(f"PlaylistModel.__init__({playlist_id=})") + log.debug(f"PlaylistModel.__init__({playlist_id=})") self.playlist_id = playlist_id super().__init__(*args, **kwargs) @@ -543,7 +543,7 @@ class PlaylistModel(QAbstractTableModel): If not given, return row number to add to end of model. """ - log.info(f"_get_new_row_number({proposed_row_number=})") + log.debug(f"_get_new_row_number({proposed_row_number=})") if proposed_row_number is None or proposed_row_number > len(self.playlist_rows): # We are adding to the end of the list @@ -840,7 +840,7 @@ class PlaylistModel(QAbstractTableModel): Move the playlist rows given to to_row and below. """ - log.info(f"move_rows({from_rows=}, {to_row_number=}") + log.debug(f"move_rows({from_rows=}, {to_row_number=}") # Build a {current_row_number: new_row_number} dictionary row_map: dict[int, int] = {} @@ -1102,7 +1102,7 @@ class PlaylistModel(QAbstractTableModel): Signal handler for when row ordering has changed """ - log.info("reset_track_sequence_row_numbers()") + log.debug("reset_track_sequence_row_numbers()") # Check the track_sequence next, now and previous plrs and # update the row number diff --git a/tests/test_playlistmodel.py b/tests/test_playlistmodel.py index 281e4c0..0e69bc7 100644 --- a/tests/test_playlistmodel.py +++ b/tests/test_playlistmodel.py @@ -28,47 +28,47 @@ from app.models import ( # noqa: E402 ) -# class TestMMMiscTracks(unittest.TestCase): -# def setUp(self): -# PLAYLIST_NAME = "tracks playlist" -# self.test_tracks = [ -# "testdata/isa.mp3", -# "testdata/isa_with_gap.mp3", -# "testdata/loser.mp3", -# "testdata/lovecats-10seconds.mp3", -# "testdata/lovecats.mp3", -# "testdata/mom.mp3", -# "testdata/sitting.mp3", -# ] +class TestMMMiscTracks(unittest.TestCase): + def setUp(self): + PLAYLIST_NAME = "tracks playlist" + self.test_tracks = [ + "testdata/isa.mp3", + "testdata/isa_with_gap.mp3", + "testdata/loser.mp3", + "testdata/lovecats-10seconds.mp3", + "testdata/lovecats.mp3", + "testdata/mom.mp3", + "testdata/sitting.mp3", + ] -# db.create_all() + db.create_all() -# # Create a playlist and model -# with db.Session() as session: -# self.playlist = Playlists(session, PLAYLIST_NAME) -# self.model = playlistmodel.PlaylistModel(self.playlist.id) + # Create a playlist and model + with db.Session() as session: + self.playlist = Playlists(session, PLAYLIST_NAME) + self.model = playlistmodel.PlaylistModel(self.playlist.id) -# for row in range(len(self.test_tracks)): -# track_path = self.test_tracks[row % len(self.test_tracks)] -# metadata = get_file_metadata(track_path) -# track = Tracks(session, **metadata) -# self.model.insert_row( -# proposed_row_number=row, track_id=track.id, note=f"{row=}" -# ) + for row in range(len(self.test_tracks)): + track_path = self.test_tracks[row % len(self.test_tracks)] + metadata = get_file_metadata(track_path) + track = Tracks(session, **metadata) + self.model.insert_row( + proposed_row_number=row, track_id=track.id, note=f"{row=}" + ) -# session.commit() + session.commit() -# def tearDown(self): -# db.drop_all() + def tearDown(self): + db.drop_all() -# def test_7_row_playlist(self): -# # Test auto-created playlist + def test_7_row_playlist(self): + # Test auto-created playlist -# assert self.model.rowCount() == 7 -# assert max(self.model.playlist_rows.keys()) == 6 -# for row in range(self.model.rowCount()): -# assert row in self.model.playlist_rows -# assert self.model.playlist_rows[row].plr_rownum == row + assert self.model.rowCount() == 7 + assert max(self.model.playlist_rows.keys()) == 6 + for row in range(self.model.rowCount()): + assert row in self.model.playlist_rows + assert self.model.playlist_rows[row].plr_rownum == row class TestMMMiscRowMove(unittest.TestCase): @@ -82,7 +82,6 @@ class TestMMMiscRowMove(unittest.TestCase): self.playlist = Playlists(session, PLAYLIST_NAME) self.model = playlistmodel.PlaylistModel(self.playlist.id) for row in range(ROWS_TO_CREATE): - print(f"{row=}") self.model.insert_row(proposed_row_number=row, note=str(row)) session.commit() From c5595bb61b2a9df50a57119f3da05e9924de9c4f Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Fri, 5 Apr 2024 16:42:02 +0100 Subject: [PATCH 6/7] All tests working --- app/playlistmodel.py | 10 +- tests/test_helpers.py | 14 + tests/test_models.py | 7 +- tests/test_playlistmodel.py | 597 +++++++++--------- ..._test_playlists.py => x_test_playlists.py} | 0 5 files changed, 309 insertions(+), 319 deletions(-) rename tests/{X_test_playlists.py => x_test_playlists.py} (100%) diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 66b2477..0cd6652 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -155,7 +155,7 @@ class PlaylistModel(QAbstractTableModel): Add track to existing header row """ - log.info(f"add_track_to_header({row_number=}, {track_id=}, {note=}") + log.debug(f"add_track_to_header({row_number=}, {track_id=}, {note=}") # Get existing row try: @@ -904,7 +904,7 @@ class PlaylistModel(QAbstractTableModel): Move the playlist rows given to to_row and below of to_playlist. """ - log.info( + log.debug( f"move_rows_between_playlists({from_rows=}, {to_row_number=}, {to_playlist_id=}" ) @@ -1134,7 +1134,7 @@ class PlaylistModel(QAbstractTableModel): return: [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]] """ - log.info(f"_reversed_contiguous_row_groups({row_numbers=} called") + log.debug(f"_reversed_contiguous_row_groups({row_numbers=} called") result: List[List[int]] = [] temp: List[int] = [] @@ -1150,7 +1150,7 @@ class PlaylistModel(QAbstractTableModel): result.append(temp) result.reverse() - log.info(f"_reversed_contiguous_row_groups() returned: {result=}") + log.debug(f"_reversed_contiguous_row_groups() returned: {result=}") return result def rowCount(self, index: QModelIndex = QModelIndex()) -> int: @@ -1163,7 +1163,7 @@ class PlaylistModel(QAbstractTableModel): Signal handler for when row ordering has changed """ - log.info(f"row_order_changed({playlist_id=}) {self.playlist_id=}") + log.debug(f"row_order_changed({playlist_id=}) {self.playlist_id=}") # Only action if this is for us if playlist_id != self.playlist_id: diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 08cf6df..141db58 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,5 +1,7 @@ # Standard library imports import datetime as dt +import shutil +import tempfile import unittest # PyQt imports @@ -14,6 +16,7 @@ from helpers import ( get_relative_date, leading_silence, ms_to_mmss, + normalise_track, ) @@ -85,3 +88,14 @@ class TestMMHelpers(unittest.TestCase): assert ms_to_mmss(None) == "-" assert ms_to_mmss(59600) == "0:59" assert ms_to_mmss((5 * 60 * 1000) + 23000) == "5:23" + + def test_normalise(self): + """Make copies to normalise to avoid corrupting source""" + + _, mp3_temp_path = tempfile.mkstemp(suffix=".mp3") + shutil.copyfile("testdata/isa.mp3", mp3_temp_path) + normalise_track(mp3_temp_path) + + _, flac_temp_path = tempfile.mkstemp(suffix=".flac") + shutil.copyfile("testdata/isa.flac", flac_temp_path) + normalise_track(flac_temp_path) diff --git a/tests/test_models.py b/tests/test_models.py index bbad41c..3901649 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -35,8 +35,6 @@ class TestMMModels(unittest.TestCase): track1_path = "testdata/isa.mp3" metadata1 = helpers.get_file_metadata(track1_path) self.track1 = Tracks(session, **metadata1) - # Test repr - _ = str(self.track1) track2_path = "testdata/mom.mp3" metadata2 = helpers.get_file_metadata(track2_path) @@ -45,6 +43,11 @@ class TestMMModels(unittest.TestCase): def tearDown(self): db.drop_all() + def test_track_repr(self): + with db.Session() as session: + session.add(self.track1) + _ =str(self.track1) + def test_notecolours_get_colour(self): """Create a colour record and retrieve all colours""" diff --git a/tests/test_playlistmodel.py b/tests/test_playlistmodel.py index 0e69bc7..35e9242 100644 --- a/tests/test_playlistmodel.py +++ b/tests/test_playlistmodel.py @@ -39,6 +39,7 @@ class TestMMMiscTracks(unittest.TestCase): "testdata/lovecats.mp3", "testdata/mom.mp3", "testdata/sitting.mp3", + "testdata/wrb.flac", ] db.create_all() @@ -61,27 +62,80 @@ class TestMMMiscTracks(unittest.TestCase): def tearDown(self): db.drop_all() - def test_7_row_playlist(self): + def test_8_row_playlist(self): # Test auto-created playlist - assert self.model.rowCount() == 7 - assert max(self.model.playlist_rows.keys()) == 6 + assert self.model.rowCount() == 8 + assert max(self.model.playlist_rows.keys()) == 7 for row in range(self.model.rowCount()): assert row in self.model.playlist_rows assert self.model.playlist_rows[row].plr_rownum == row + def test_timing_one_track(self): + START_ROW = 0 + END_ROW = 2 + + self.model.insert_row(proposed_row_number=START_ROW, note="start+") + self.model.insert_row(proposed_row_number=END_ROW, note="-") + + prd = self.model.playlist_rows[START_ROW] + qv_value = self.model.display_role(START_ROW, playlistmodel.HEADER_NOTES_COLUMN, prd) + assert qv_value.value() == "start [1 tracks, 4:23 unplayed]" + + +class TestMMMiscNoPlaylist(unittest.TestCase): + + PLAYLIST_NAME = "tracks playlist" + test_tracks = [ + "testdata/isa.mp3", + "testdata/isa_with_gap.mp3", + "testdata/loser.mp3", + "testdata/lovecats-10seconds.mp3", + "testdata/lovecats.mp3", + "testdata/mom.mp3", + "testdata/sitting.mp3", + ] + + def setUp(self): + db.create_all() + + def tearDown(self): + db.drop_all() + + def test_insert_track_new_playlist(self): + # insert a track into a new playlist + with db.Session() as session: + playlist = Playlists(session, self.PLAYLIST_NAME) + # Create a model + model = playlistmodel.PlaylistModel(playlist.id) + # test repr + _ = str(model) + + track_path = self.test_tracks[0] + metadata = get_file_metadata(track_path) + track = Tracks(session, **metadata) + model.insert_row(proposed_row_number=0, track_id=track.id) + + prd = model.playlist_rows[model.rowCount() - 1] + # test repr + _ = str(prd) + + assert ( + model.edit_role(model.rowCount() - 1, playlistmodel.Col.TITLE.value, prd) + == metadata["title"] + ) class TestMMMiscRowMove(unittest.TestCase): - def setUp(self): - PLAYLIST_NAME = "rowmove playlist" - ROWS_TO_CREATE = 11 + PLAYLIST_NAME = "rowmove playlist" + ROWS_TO_CREATE = 11 + def setUp(self): db.create_all() with db.Session() as session: - self.playlist = Playlists(session, PLAYLIST_NAME) + self.playlist = Playlists(session, self.PLAYLIST_NAME) self.model = playlistmodel.PlaylistModel(self.playlist.id) - for row in range(ROWS_TO_CREATE): + for row in range(self.ROWS_TO_CREATE): self.model.insert_row(proposed_row_number=row, note=str(row)) session.commit() @@ -105,311 +159,230 @@ class TestMMMiscRowMove(unittest.TestCase): elif row == 5: assert self.model.playlist_rows[row].note == str(3) + def test_move_rows_test3(self): + # move row 4 to row 3 + + self.model.move_rows([4], 3) + + # Check we have all rows and plr_rownums are correct + for row in range(self.model.rowCount()): + assert row in self.model.playlist_rows + assert self.model.playlist_rows[row].plr_rownum == row + if row not in [3, 4]: + assert self.model.playlist_rows[row].note == str(row) + elif row == 3: + assert self.model.playlist_rows[row].note == str(4) + elif row == 4: + assert self.model.playlist_rows[row].note == str(3) + + def test_move_rows_test4(self): + # move row 4 to row 2 + + self.model.move_rows([4], 2) + + # Check we have all rows and plr_rownums are correct + for row in range(self.model.rowCount()): + assert row in self.model.playlist_rows + assert self.model.playlist_rows[row].plr_rownum == row + if row not in [2, 3, 4]: + assert self.model.playlist_rows[row].note == str(row) + elif row == 2: + assert self.model.playlist_rows[row].note == str(4) + elif row == 3: + assert self.model.playlist_rows[row].note == str(2) + elif row == 4: + assert self.model.playlist_rows[row].note == str(3) + + def test_move_rows_test5(self): + # move rows [1, 4, 5, 10] → 8 + + self.model.move_rows([1, 4, 5, 10], 8) + + # Check we have all rows and plr_rownums are correct + new_order = [] + for row in range(self.model.rowCount()): + assert row in self.model.playlist_rows + assert self.model.playlist_rows[row].plr_rownum == row + new_order.append(int(self.model.playlist_rows[row].note)) + assert new_order == [0, 2, 3, 6, 7, 8, 9, 1, 4, 5, 10] + + def test_move_rows_test6(self): + # move rows [3, 6] → 5 + + self.model.move_rows([3, 6], 5) + + # Check we have all rows and plr_rownums are correct + new_order = [] + for row in range(self.model.rowCount()): + assert row in self.model.playlist_rows + assert self.model.playlist_rows[row].plr_rownum == row + new_order.append(int(self.model.playlist_rows[row].note)) + assert new_order == [0, 1, 2, 4, 5, 3, 6, 7, 8, 9, 10] + + def test_move_rows_test7(self): + # move rows [3, 5, 6] → 8 + + self.model.move_rows([3, 5, 6], 8) + + # Check we have all rows and plr_rownums are correct + new_order = [] + for row in range(self.model.rowCount()): + assert row in self.model.playlist_rows + assert self.model.playlist_rows[row].plr_rownum == row + new_order.append(int(self.model.playlist_rows[row].note)) + assert new_order == [0, 1, 2, 4, 7, 8, 9, 10, 3, 5, 6] + + def test_move_rows_test8(self): + # move rows [7, 8, 10] → 5 + + self.model.move_rows([7, 8, 10], 5) + + # Check we have all rows and plr_rownums are correct + new_order = [] + for row in range(self.model.rowCount()): + assert row in self.model.playlist_rows + assert self.model.playlist_rows[row].plr_rownum == row + new_order.append(int(self.model.playlist_rows[row].note)) + assert new_order == [0, 1, 2, 3, 4, 7, 8, 10, 5, 6, 9] + + def test_insert_header_row_end(self): + # insert header row at end of playlist + + note_text = "test text" + + assert self.model.rowCount() == self.ROWS_TO_CREATE + self.model.insert_row(proposed_row_number=None, note=note_text) + assert self.model.rowCount() == self.ROWS_TO_CREATE + 1 + prd = self.model.playlist_rows[self.model.rowCount() - 1] + # Test against edit_role because display_role for headers is + # handled differently (sets up row span) + assert ( + self.model.edit_role( + self.model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd + ) + == note_text + ) + + def test_insert_header_row_middle(self): + # insert header row in middle of playlist + + note_text = "test text" + insert_row = 6 + + self.model.insert_row(proposed_row_number=insert_row, note=note_text) + assert self.model.rowCount() == self.ROWS_TO_CREATE + 1 + prd = self.model.playlist_rows[insert_row] + # Test against edit_role because display_role for headers is + # handled differently (sets up row span) + assert ( + self.model.edit_role( + self.model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd + ) + == note_text + ) + + def test_add_track_to_header(self): + note_text = "test text" + insert_row = 6 + + self.model.insert_row(proposed_row_number=insert_row, note=note_text) + assert self.model.rowCount() == self.ROWS_TO_CREATE + 1 + + prd = self.model.playlist_rows[1] + self.model.add_track_to_header(insert_row, prd.track_id) + + def test_reverse_row_groups_one_row(self): + + rows_to_move = [3] + + result = self.model._reversed_contiguous_row_groups(rows_to_move) + + assert len(result) == 1 + assert result[0] == [3] + + def test_reverse_row_groups_multiple_row(self): + + rows_to_move = [2, 3, 4, 5, 7, 9, 10, 13, 17, 20, 21] + + result = self.model._reversed_contiguous_row_groups(rows_to_move) + + assert result == [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]] + + def test_move_one_row_between_playlists_to_end(self): + from_rows = [3] + to_row = self.ROWS_TO_CREATE + destination_playlist = "destination" + + model_src = self.model + with db.Session() as session: + playlist_dst = Playlists(session, destination_playlist) + model_dst = playlistmodel.PlaylistModel(playlist_dst.id) + for row in range(self.ROWS_TO_CREATE): + model_dst.insert_row(proposed_row_number=row, note=str(row)) + + model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) + model_dst.refresh_data(session) + + assert len(model_src.playlist_rows) == self.ROWS_TO_CREATE - len(from_rows) + assert len(model_dst.playlist_rows) == self.ROWS_TO_CREATE + len(from_rows) + assert sorted([a.plr_rownum for a in model_src.playlist_rows.values()]) == list( + range(len(model_src.playlist_rows)) + ) + + def test_move_one_row_between_playlists_to_middle(self): + from_rows = [3] + to_row = 2 + destination_playlist = "destination" + + model_src = self.model + with db.Session() as session: + playlist_dst = Playlists(session, destination_playlist) + model_dst = playlistmodel.PlaylistModel(playlist_dst.id) + for row in range(self.ROWS_TO_CREATE): + model_dst.insert_row(proposed_row_number=row, note=str(row)) + + model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) + model_dst.refresh_data(session) + + # Check the rows of the destination model + row_notes = [] + for row_number in range(model_dst.rowCount()): + index = model_dst.index( + row_number, playlistmodel.Col.TITLE.value, QModelIndex() + ) + row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value()) + + assert len(model_src.playlist_rows) == self.ROWS_TO_CREATE - len(from_rows) + assert len(model_dst.playlist_rows) == self.ROWS_TO_CREATE + len(from_rows) + assert [int(a) for a in row_notes] == [0, 1, 3, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + def test_move_multiple_rows_between_playlists_to_end(self): + + from_rows = [1, 3, 4] + to_row = 2 + destination_playlist = "destination" + + model_src = self.model + with db.Session() as session: + playlist_dst = Playlists(session, destination_playlist) + model_dst = playlistmodel.PlaylistModel(playlist_dst.id) + for row in range(self.ROWS_TO_CREATE): + model_dst.insert_row(proposed_row_number=row, note=str(row)) + + model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) + model_dst.refresh_data(session) + + # Check the rows of the destination model + row_notes = [] + for row_number in range(model_dst.rowCount()): + index = model_dst.index( + row_number, playlistmodel.Col.TITLE.value, QModelIndex() + ) + row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value()) -# def test_move_rows_test3(monkeypatch, session): -# # move row 4 to row 3 - -# monkeypatch.setattr(playlistmodel, "Session", session) - -# model = create_model_with_playlist_rows(session, 11) -# model.move_rows([4], 3) - -# # Check we have all rows and plr_rownums are correct -# for row in range(model.rowCount()): -# assert row in model.playlist_rows -# assert model.playlist_rows[row].plr_rownum == row -# if row not in [3, 4]: -# assert model.playlist_rows[row].note == str(row) -# elif row == 3: -# assert model.playlist_rows[row].note == str(4) -# elif row == 4: -# assert model.playlist_rows[row].note == str(3) - - -# def test_move_rows_test4(monkeypatch, session): -# # move row 4 to row 2 - -# monkeypatch.setattr(playlistmodel, "Session", session) - -# model = create_model_with_playlist_rows(session, 11) -# model.move_rows([4], 2) - -# # Check we have all rows and plr_rownums are correct -# for row in range(model.rowCount()): -# assert row in model.playlist_rows -# assert model.playlist_rows[row].plr_rownum == row -# if row not in [2, 3, 4]: -# assert model.playlist_rows[row].note == str(row) -# elif row == 2: -# assert model.playlist_rows[row].note == str(4) -# elif row == 3: -# assert model.playlist_rows[row].note == str(2) -# elif row == 4: -# assert model.playlist_rows[row].note == str(3) - - -# def test_move_rows_test5(monkeypatch, session): -# # move rows [1, 4, 5, 10] → 8 - -# monkeypatch.setattr(playlistmodel, "Session", session) - -# model = create_model_with_playlist_rows(session, 11) -# model.move_rows([1, 4, 5, 10], 8) - -# # Check we have all rows and plr_rownums are correct -# new_order = [] -# for row in range(model.rowCount()): -# assert row in model.playlist_rows -# assert model.playlist_rows[row].plr_rownum == row -# new_order.append(int(model.playlist_rows[row].note)) -# assert new_order == [0, 2, 3, 6, 7, 8, 9, 1, 4, 5, 10] - - -# def test_move_rows_test6(monkeypatch, session): -# # move rows [3, 6] → 5 - -# monkeypatch.setattr(playlistmodel, "Session", session) - -# model = create_model_with_playlist_rows(session, 11) -# model.move_rows([3, 6], 5) - -# # Check we have all rows and plr_rownums are correct -# new_order = [] -# for row in range(model.rowCount()): -# assert row in model.playlist_rows -# assert model.playlist_rows[row].plr_rownum == row -# new_order.append(int(model.playlist_rows[row].note)) -# assert new_order == [0, 1, 2, 4, 5, 3, 6, 7, 8, 9, 10] - - -# def test_move_rows_test7(monkeypatch, session): -# # move rows [3, 5, 6] → 8 - -# monkeypatch.setattr(playlistmodel, "Session", session) - -# model = create_model_with_playlist_rows(session, 11) -# model.move_rows([3, 5, 6], 8) - -# # Check we have all rows and plr_rownums are correct -# new_order = [] -# for row in range(model.rowCount()): -# assert row in model.playlist_rows -# assert model.playlist_rows[row].plr_rownum == row -# new_order.append(int(model.playlist_rows[row].note)) -# assert new_order == [0, 1, 2, 4, 7, 8, 9, 10, 3, 5, 6] - - -# def test_move_rows_test8(monkeypatch, session): -# # move rows [7, 8, 10] → 5 - -# monkeypatch.setattr(playlistmodel, "Session", session) - -# model = create_model_with_playlist_rows(session, 11) -# model.move_rows([7, 8, 10], 5) - -# # Check we have all rows and plr_rownums are correct -# new_order = [] -# for row in range(model.rowCount()): -# assert row in model.playlist_rows -# assert model.playlist_rows[row].plr_rownum == row -# new_order.append(int(model.playlist_rows[row].note)) -# assert new_order == [0, 1, 2, 3, 4, 7, 8, 10, 5, 6, 9] - - -# def test_insert_header_row_end(monkeypatch, session): -# # insert header row at end of playlist - -# monkeypatch.setattr(playlistmodel, "Session", session) -# note_text = "test text" -# initial_row_count = 11 - -# model = create_model_with_playlist_rows(session, initial_row_count) -# model.insert_row(proposed_row_number=None, note=note_text) -# assert model.rowCount() == initial_row_count + 1 -# prd = model.playlist_rows[model.rowCount() - 1] -# # Test against edit_role because display_role for headers is -# # handled differently (sets up row span) -# assert ( -# model.edit_role(model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd) -# == note_text -# ) - - -# def test_insert_header_row_middle(monkeypatch, session): -# # insert header row in middle of playlist - -# monkeypatch.setattr(playlistmodel, "Session", session) -# note_text = "test text" -# initial_row_count = 11 -# insert_row = 6 - -# model = create_model_with_playlist_rows(session, initial_row_count) -# model.insert_row(proposed_row_number=insert_row, note=note_text) -# assert model.rowCount() == initial_row_count + 1 -# prd = model.playlist_rows[insert_row] -# # Test against edit_role because display_role for headers is -# # handled differently (sets up row span) -# assert ( -# model.edit_role(model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd) -# == note_text -# ) - - -# def test_add_track_to_header(monkeypatch, session): -# monkeypatch.setattr(playlistmodel, "Session", session) -# note_text = "test text" -# initial_row_count = 11 -# insert_row = 6 - -# model = create_model_with_playlist_rows(session, initial_row_count) -# model.insert_row(proposed_row_number=insert_row, note=note_text) -# assert model.rowCount() == initial_row_count + 1 - -# prd = model.playlist_rows[1] -# model.add_track_to_header(insert_row, prd.track_id) - - -# def test_create_model_with_tracks(monkeypatch, session): -# monkeypatch.setattr(playlistmodel, "Session", session) -# model = create_model_with_tracks(session) -# assert len(model.playlist_rows) == len(self.test_tracks) - - -# def test_timing_one_track(monkeypatch, session): -# START_ROW = 0 -# END_ROW = 2 - -# monkeypatch.setattr(playlistmodel, "Session", session) -# model = create_model_with_tracks(session) - -# model.insert_row(proposed_row_number=START_ROW, note="start+") -# model.insert_row(proposed_row_number=END_ROW, note="-") - -# prd = model.playlist_rows[START_ROW] -# qv_value = model.display_role(START_ROW, playlistmodel.HEADER_NOTES_COLUMN, prd) -# assert qv_value.value() == "start [1 tracks, 4:23 unplayed]" - - -# def test_insert_track_new_playlist(monkeypatch, session): -# # insert a track into a new playlist - -# monkeypatch.setattr(playlistmodel, "Session", session) - -# playlist = Playlists(session, "test playlist") -# # Create a model -# model = playlistmodel.PlaylistModel(playlist.id) - -# track_path = self.test_tracks[0] -# metadata = get_file_metadata(track_path) -# track = Tracks(session, **metadata) -# model.insert_row(proposed_row_number=0, track_id=track.id) - -# prd = model.playlist_rows[model.rowCount() - 1] -# assert ( -# model.edit_role(model.rowCount() - 1, playlistmodel.Col.TITLE.value, prd) -# == metadata["title"] -# ) - - -# def test_reverse_row_groups_one_row(monkeypatch, session): -# monkeypatch.setattr(playlistmodel, "Session", session) - -# rows_to_move = [3] - -# model_src = create_model_with_playlist_rows(session, 5, name="source") -# result = model_src._reversed_contiguous_row_groups(rows_to_move) - -# assert len(result) == 1 -# assert result[0] == [3] - - -# def test_reverse_row_groups_multiple_row(monkeypatch, session): -# monkeypatch.setattr(playlistmodel, "Session", session) - -# rows_to_move = [2, 3, 4, 5, 7, 9, 10, 13, 17, 20, 21] - -# model_src = create_model_with_playlist_rows(session, 5, name="source") -# result = model_src._reversed_contiguous_row_groups(rows_to_move) - -# assert result == [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]] - - -# def test_move_one_row_between_playlists_to_end(monkeypatch, session): -# monkeypatch.setattr(playlistmodel, "Session", session) - -# create_rowcount = 5 -# from_rows = [3] -# to_row = create_rowcount - -# model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") -# model_dst = create_model_with_playlist_rows( -# session, create_rowcount, name="destination" -# ) - -# model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) -# model_dst.refresh_data(session) - -# assert len(model_src.playlist_rows) == create_rowcount - len(from_rows) -# assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows) -# assert sorted([a.plr_rownum for a in model_src.playlist_rows.values()]) == list( -# range(len(model_src.playlist_rows)) -# ) - - -# def test_move_one_row_between_playlists_to_middle(monkeypatch, session): -# monkeypatch.setattr(playlistmodel, "Session", session) - -# create_rowcount = 5 -# from_rows = [3] -# to_row = 2 - -# model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") -# model_dst = create_model_with_playlist_rows( -# session, create_rowcount, name="destination" -# ) - -# model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) -# model_dst.refresh_data(session) - -# # Check the rows of the destination model -# row_notes = [] -# for row_number in range(model_dst.rowCount()): -# index = model_dst.index( -# row_number, playlistmodel.Col.TITLE.value, QModelIndex() -# ) -# row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value()) - -# assert len(model_src.playlist_rows) == create_rowcount - len(from_rows) -# assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows) -# assert [int(a) for a in row_notes] == [0, 1, 3, 2, 3, 4] - - -# def test_move_multiple_rows_between_playlists_to_end(monkeypatch, session): -# monkeypatch.setattr(playlistmodel, "Session", session) - -# create_rowcount = 5 -# from_rows = [1, 3, 4] -# to_row = 2 - -# model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") -# model_dst = create_model_with_playlist_rows( -# session, create_rowcount, name="destination" -# ) - -# model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) -# model_dst.refresh_data(session) - -# # Check the rows of the destination model -# row_notes = [] -# for row_number in range(model_dst.rowCount()): -# index = model_dst.index( -# row_number, playlistmodel.Col.TITLE.value, QModelIndex() -# ) -# row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value()) - -# assert len(model_src.playlist_rows) == create_rowcount - len(from_rows) -# assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows) -# assert [int(a) for a in row_notes] == [0, 1, 3, 4, 1, 2, 3, 4] + assert len(model_src.playlist_rows) == self.ROWS_TO_CREATE - len(from_rows) + assert len(model_dst.playlist_rows) == self.ROWS_TO_CREATE + len(from_rows) + assert [int(a) for a in row_notes] == [0, 1, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # # def test_edit_header(monkeypatch, session): # edit header row in middle of playlist diff --git a/tests/X_test_playlists.py b/tests/x_test_playlists.py similarity index 100% rename from tests/X_test_playlists.py rename to tests/x_test_playlists.py From 6624ac8f31974f507ce03904f2c5b15a75c0c728 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Fri, 5 Apr 2024 17:29:06 +0100 Subject: [PATCH 7/7] Fix up db import --- app/musicmuster.py | 3 +-- app/playlists.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/musicmuster.py b/app/musicmuster.py index 1c56bba..6a5c356 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -59,10 +59,9 @@ from classes import ( PlaylistTrack, ) from config import Config -from dbtables import db from dialogs import TrackSelectDialog from log import log -from models import Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks +from models import db, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks from playlistmodel import PlaylistModel, PlaylistProxyModel from playlists import PlaylistTab from ui import icons_rc # noqa F401 diff --git a/app/playlists.py b/app/playlists.py index ea8791e..22c5f8a 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -36,7 +36,6 @@ from PyQt6.QtWidgets import ( # App imports from classes import MusicMusterSignals, track_sequence from config import Config -from dbtables import db from dialogs import TrackSelectDialog from helpers import ( ask_yes_no, @@ -45,7 +44,7 @@ from helpers import ( show_warning, ) from log import log -from models import Settings +from models import db, Settings from playlistmodel import PlaylistModel, PlaylistProxyModel if TYPE_CHECKING: