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 +# # )