Migrated to Alchemical

This commit is contained in:
Keith Edmunds 2024-04-05 08:37:36 +01:00
parent 6fd541060e
commit 3821a7061b
13 changed files with 1036 additions and 938 deletions

2
.envrc
View File

@ -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

View File

@ -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)

View File

@ -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"<NoteColour(id={self.id}, substring={self.substring}, "
f"<NoteColours(id={self.id}, substring={self.substring}, "
f"colour={self.colour}>"
)
@ -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"<Track(id={self.id}, title={self.title}, "
f"artist={self.artist}, path={self.path}>"
)
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)

View File

@ -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

View File

@ -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

View File

@ -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

345
poetry.lock generated
View File

@ -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"

View File

@ -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"]

View File

@ -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"

View File

@ -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

View File

@ -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

View File

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