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" export MM_DB="mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod"
else else
export MM_ENV="DEVELOPMENT" 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" export PYTHONBREAKPOINT="pudb.set_trace"
fi fi

View File

@ -35,7 +35,7 @@ class Config(object):
COLOUR_WARNING_TIMER = "#ffc107" COLOUR_WARNING_TIMER = "#ffc107"
DBFS_SILENCE = -50 DBFS_SILENCE = -50
DEBUG_FUNCTIONS: List[Optional[str]] = [] DEBUG_FUNCTIONS: List[Optional[str]] = []
DEBUG_MODULES: List[Optional[str]] = ["dbconfig"] DEBUG_MODULES: List[Optional[str]] = []
DEFAULT_COLUMN_WIDTH = 200 DEFAULT_COLUMN_WIDTH = 200
DISPLAY_SQL = False DISPLAY_SQL = False
EPOCH = dt.datetime(1970, 1, 1) EPOCH = dt.datetime(1970, 1, 1)

View File

@ -1,7 +1,8 @@
# Standard library imports # Standard library imports
import os
import sys
from typing import List, Optional from typing import List, Optional
import datetime as dt import datetime as dt
import os
# PyQt imports # PyQt imports
@ -24,8 +25,6 @@ from sqlalchemy.orm import (
# Database classes # Database classes
# Note: initialisation of the 'db' variable is at the foot of this
# module.
class CartsTable(Model): class CartsTable(Model):
__tablename__ = "carts" __tablename__ = "carts"
@ -56,7 +55,7 @@ class NoteColoursTable(Model):
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"<NoteColour(id={self.id}, substring={self.substring}, " f"<NoteColours(id={self.id}, substring={self.substring}, "
f"colour={self.colour}>" f"colour={self.colour}>"
) )
@ -67,7 +66,7 @@ class PlaydatesTable(Model):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
lastplayed: Mapped[dt.datetime] = mapped_column(index=True) lastplayed: Mapped[dt.datetime] = mapped_column(index=True)
track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id")) 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: def __repr__(self) -> str:
return ( return (
@ -91,10 +90,10 @@ class PlaylistsTable(Model):
is_template: Mapped[bool] = mapped_column(default=False) is_template: Mapped[bool] = mapped_column(default=False)
deleted: Mapped[bool] = mapped_column(default=False) deleted: Mapped[bool] = mapped_column(default=False)
rows: Mapped[List["PlaylistRowsTable"]] = relationship( rows: Mapped[List["PlaylistRowsTable"]] = relationship(
"PlaylistRows", "PlaylistRowsTable",
back_populates="playlist", back_populates="playlist",
cascade="all, delete-orphan", cascade="all, delete-orphan",
order_by="PlaylistRows.plr_rownum", order_by="PlaylistRowsTable.plr_rownum",
) )
def __repr__(self) -> str: def __repr__(self) -> str:
@ -116,7 +115,7 @@ class PlaylistRowsTable(Model):
playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows") playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows")
track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id")) track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id"))
track: Mapped["TracksTable"] = relationship( track: Mapped["TracksTable"] = relationship(
"Tracks", "TracksTable",
back_populates="playlistrows", back_populates="playlistrows",
) )
played: Mapped[bool] = mapped_column( played: Mapped[bool] = mapped_column(
@ -163,11 +162,11 @@ class TracksTable(Model):
silence_at: Mapped[int] = mapped_column(index=False) silence_at: Mapped[int] = mapped_column(index=False)
start_gap: Mapped[int] = mapped_column(index=False) start_gap: Mapped[int] = mapped_column(index=False)
playlistrows: Mapped[List[PlaylistRowsTable]] = relationship( playlistrows: Mapped[List[PlaylistRowsTable]] = relationship(
"PlaylistRows", back_populates="track" "PlaylistRowsTable", back_populates="track"
) )
playlists = association_proxy("playlistrows", "playlist") playlists = association_proxy("playlistrows", "playlist")
playdates: Mapped[List[PlaydatesTable]] = relationship( playdates: Mapped[List[PlaydatesTable]] = relationship(
"Playdates", "PlaydatesTable",
back_populates="track", back_populates="track",
lazy="joined", lazy="joined",
) )
@ -177,11 +176,3 @@ class TracksTable(Model):
f"<Track(id={self.id}, title={self.title}, " f"<Track(id={self.id}, title={self.title}, "
f"artist={self.artist}, path={self.path}>" 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 # Standard library imports
from typing import List, Optional, Sequence from typing import List, Optional, Sequence
import datetime as dt import datetime as dt
import os
import re import re
import sys
# PyQt imports # PyQt imports
# Third party imports # Third party imports
from alchemical import Alchemical # type:ignore
from sqlalchemy import ( from sqlalchemy import (
bindparam, bindparam,
delete, delete,
@ -13,16 +16,10 @@ from sqlalchemy import (
select, select,
update, update,
) )
from sqlalchemy.exc import ( from sqlalchemy.exc import IntegrityError
IntegrityError, from sqlalchemy.orm.exc import NoResultFound
) from sqlalchemy.orm import joinedload
from sqlalchemy.orm import ( from sqlalchemy.orm.session import Session
joinedload,
scoped_session,
)
from sqlalchemy.orm.exc import (
NoResultFound,
)
# App imports # App imports
import dbtables import dbtables
@ -30,12 +27,21 @@ from config import Config
from log import log 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 # Database classes
class Carts(dbtables.CartsTable): class Carts(dbtables.CartsTable):
def __init__( def __init__(
self, self,
session: scoped_session, session: Session,
cart_number: int, cart_number: int,
name: str, name: str,
duration: Optional[int] = None, duration: Optional[int] = None,
@ -58,7 +64,7 @@ class NoteColours(dbtables.NoteColoursTable):
def __init__( def __init__(
self, self,
session: scoped_session, session: Session,
substring: str, substring: str,
colour: str, colour: str,
enabled: bool = True, enabled: bool = True,
@ -77,7 +83,7 @@ class NoteColours(dbtables.NoteColoursTable):
session.flush() session.flush()
@classmethod @classmethod
def get_all(cls, session: scoped_session) -> Sequence["NoteColours"]: def get_all(cls, session: Session) -> Sequence["NoteColours"]:
""" """
Return all records Return all records
""" """
@ -85,7 +91,7 @@ class NoteColours(dbtables.NoteColoursTable):
return session.scalars(select(cls)).all() return session.scalars(select(cls)).all()
@staticmethod @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 Parse text and return colour string if matched, else empty string
""" """
@ -118,7 +124,7 @@ class NoteColours(dbtables.NoteColoursTable):
class Playdates(dbtables.PlaydatesTable): 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""" """Record that track was played"""
self.lastplayed = dt.datetime.now() self.lastplayed = dt.datetime.now()
@ -127,7 +133,7 @@ class Playdates(dbtables.PlaydatesTable):
session.commit() session.commit()
@staticmethod @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""" """Return datetime track last played or None"""
last_played = session.execute( last_played = session.execute(
@ -145,7 +151,7 @@ class Playdates(dbtables.PlaydatesTable):
return Config.EPOCH # pragma: no cover return Config.EPOCH # pragma: no cover
@staticmethod @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 a list of Playdates objects since passed time"""
return session.scalars( return session.scalars(
@ -157,13 +163,13 @@ class Playdates(dbtables.PlaydatesTable):
class Playlists(dbtables.PlaylistsTable): class Playlists(dbtables.PlaylistsTable):
def __init__(self, session: scoped_session, name: str): def __init__(self, session: Session, name: str):
self.name = name self.name = name
session.add(self) session.add(self)
session.flush() session.flush()
@staticmethod @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 Make all tab records NULL
""" """
@ -183,7 +189,7 @@ class Playlists(dbtables.PlaylistsTable):
@classmethod @classmethod
def create_playlist_from_template( def create_playlist_from_template(
cls, session: scoped_session, template: "Playlists", playlist_name: str cls, session: Session, template: "Playlists", playlist_name: str
) -> Optional["Playlists"]: ) -> Optional["Playlists"]:
"""Create a new playlist from template""" """Create a new playlist from template"""
@ -197,7 +203,7 @@ class Playlists(dbtables.PlaylistsTable):
return playlist return playlist
def delete(self, session: scoped_session) -> None: def delete(self, session: Session) -> None:
""" """
Mark as deleted Mark as deleted
""" """
@ -206,7 +212,7 @@ class Playlists(dbtables.PlaylistsTable):
session.flush() session.flush()
@classmethod @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""" """Returns a list of all playlists ordered by last use"""
return session.scalars( return session.scalars(
@ -216,7 +222,7 @@ class Playlists(dbtables.PlaylistsTable):
).all() ).all()
@classmethod @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""" """Returns a list of all templates ordered by name"""
return session.scalars( return session.scalars(
@ -224,7 +230,7 @@ class Playlists(dbtables.PlaylistsTable):
).all() ).all()
@classmethod @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""" """Returns a list of all closed playlists ordered by last use"""
return session.scalars( return session.scalars(
@ -238,7 +244,7 @@ class Playlists(dbtables.PlaylistsTable):
).all() ).all()
@classmethod @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. Return a list of loaded playlists ordered by tab.
""" """
@ -254,7 +260,7 @@ class Playlists(dbtables.PlaylistsTable):
self.last_used = dt.datetime.now() self.last_used = dt.datetime.now()
@staticmethod @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. Return True if no playlist of this name exists else false.
""" """
@ -264,7 +270,7 @@ class Playlists(dbtables.PlaylistsTable):
is None is None
) )
def rename(self, session: scoped_session, new_name: str) -> None: def rename(self, session: Session, new_name: str) -> None:
""" """
Rename playlist Rename playlist
""" """
@ -274,7 +280,7 @@ class Playlists(dbtables.PlaylistsTable):
@staticmethod @staticmethod
def save_as_template( def save_as_template(
session: scoped_session, playlist_id: int, template_name: str session: Session, playlist_id: int, template_name: str
) -> None: ) -> None:
"""Save passed playlist as new template""" """Save passed playlist as new template"""
@ -292,7 +298,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
def __init__( def __init__(
self, self,
session: scoped_session, session: Session,
playlist_id: int, playlist_id: int,
row_number: int, row_number: int,
note: str = "", note: str = "",
@ -305,7 +311,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
self.plr_rownum = row_number self.plr_rownum = row_number
self.note = note self.note = note
session.add(self) session.add(self)
session.flush() session.commit()
def append_note(self, extra_note: str) -> None: def append_note(self, extra_note: str) -> None:
"""Append passed note to any existing note""" """Append passed note to any existing note"""
@ -317,7 +323,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
self.note = extra_note self.note = extra_note
@staticmethod @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""" """Copy playlist entries"""
src_rows = session.scalars( src_rows = session.scalars(
@ -335,7 +341,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
@classmethod @classmethod
def deep_row( def deep_row(
cls, session: scoped_session, playlist_id: int, row_number: int cls, session: Session, playlist_id: int, row_number: int
) -> "PlaylistRows": ) -> "PlaylistRows":
""" """
Return a playlist row that includes full track and lastplayed data for Return a playlist row that includes full track and lastplayed data for
@ -356,7 +362,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
@classmethod @classmethod
def deep_rows( def deep_rows(
cls, session: scoped_session, playlist_id: int cls, session: Session, playlist_id: int
) -> Sequence["PlaylistRows"]: ) -> Sequence["PlaylistRows"]:
""" """
Return a list of playlist rows that include full track and lastplayed data for Return a list of playlist rows that include full track and lastplayed data for
@ -375,7 +381,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
@staticmethod @staticmethod
def delete_higher_rows( def delete_higher_rows(
session: scoped_session, playlist_id: int, maxrow: int session: Session, playlist_id: int, maxrow: int
) -> None: ) -> None:
""" """
Delete rows in given playlist that have a higher row number Delete rows in given playlist that have a higher row number
@ -391,7 +397,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
session.flush() session.flush()
@staticmethod @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. Delete passed row in given playlist.
""" """
@ -404,7 +410,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
) )
@staticmethod @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 Ensure the row numbers for passed playlist have no gaps
""" """
@ -423,7 +429,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
@classmethod @classmethod
def plrids_to_plrs( 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"]: ) -> Sequence["PlaylistRows"]:
""" """
Take a list of PlaylistRows ids and return a list of corresponding Take a list of PlaylistRows ids and return a list of corresponding
@ -439,7 +445,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
return plrs return plrs
@staticmethod @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 the last used row for playlist, or None if no rows"""
return session.execute( return session.execute(
@ -450,7 +456,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
@staticmethod @staticmethod
def get_track_plr( def get_track_plr(
session: scoped_session, track_id: int, playlist_id: int session: Session, track_id: int, playlist_id: int
) -> Optional["PlaylistRows"]: ) -> Optional["PlaylistRows"]:
"""Return first matching PlaylistRows object or None""" """Return first matching PlaylistRows object or None"""
@ -465,7 +471,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
@classmethod @classmethod
def get_played_rows( def get_played_rows(
cls, session: scoped_session, playlist_id: int cls, session: Session, playlist_id: int
) -> Sequence["PlaylistRows"]: ) -> Sequence["PlaylistRows"]:
""" """
For passed playlist, return a list of rows that For passed playlist, return a list of rows that
@ -483,7 +489,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
@classmethod @classmethod
def get_rows_with_tracks( def get_rows_with_tracks(
cls, cls,
session: scoped_session, session: Session,
playlist_id: int, playlist_id: int,
) -> Sequence["PlaylistRows"]: ) -> Sequence["PlaylistRows"]:
""" """
@ -500,7 +506,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
@classmethod @classmethod
def get_unplayed_rows( def get_unplayed_rows(
cls, session: scoped_session, playlist_id: int cls, session: Session, playlist_id: int
) -> Sequence["PlaylistRows"]: ) -> Sequence["PlaylistRows"]:
""" """
For passed playlist, return a list of playlist rows that For passed playlist, return a list of playlist rows that
@ -521,14 +527,14 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
@classmethod @classmethod
def insert_row( 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": ) -> "PlaylistRows":
cls.move_rows_down(session, playlist_id, new_row_number, 1) cls.move_rows_down(session, playlist_id, new_row_number, 1)
return cls(session, playlist_id, new_row_number) return cls(session, playlist_id, new_row_number)
@staticmethod @staticmethod
def move_rows_down( 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: ) -> None:
""" """
Create space to insert move_by additional rows by incremented row Create space to insert move_by additional rows by incremented row
@ -548,7 +554,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
@staticmethod @staticmethod
def update_plr_rownumbers( 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: ) -> None:
""" """
Take a {plrid: plr_rownum} dictionary and update the row numbers accordingly Take a {plrid: plr_rownum} dictionary and update the row numbers accordingly
@ -569,6 +575,11 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
class Settings(dbtables.SettingsTable): class Settings(dbtables.SettingsTable):
def __init__(self, session: Session, name: str):
self.name = name
session.add(self)
session.flush()
@classmethod @classmethod
def all_as_dict(cls, session): def all_as_dict(cls, session):
""" """
@ -584,7 +595,7 @@ class Settings(dbtables.SettingsTable):
return result return result
@classmethod @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""" """Get setting for an integer or return new setting record"""
try: try:
@ -593,7 +604,7 @@ class Settings(dbtables.SettingsTable):
except NoResultFound: except NoResultFound:
return Settings(session, name) 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(): for key, value in data.items():
assert hasattr(self, key) assert hasattr(self, key)
setattr(self, key, value) setattr(self, key, value)
@ -610,7 +621,7 @@ class Tracks(dbtables.TracksTable):
def __init__( def __init__(
self, self,
session: scoped_session, session: Session,
path: str, path: str,
title: str, title: str,
artist: str, artist: str,
@ -646,7 +657,7 @@ class Tracks(dbtables.TracksTable):
return session.scalars(select(cls)).unique().all() return session.scalars(select(cls)).unique().all()
@classmethod @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. Return track with passed path, or None.
""" """
@ -661,7 +672,7 @@ class Tracks(dbtables.TracksTable):
return None return None
@classmethod @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 Search case-insenstively for artists containing str
@ -682,7 +693,7 @@ class Tracks(dbtables.TracksTable):
) )
@classmethod @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 Search case-insenstively for titles containing str

View File

@ -2,21 +2,14 @@
# Allow forward reference to PlaylistModel # Allow forward reference to PlaylistModel
from __future__ import annotations 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 enum import auto, Enum
from operator import attrgetter from operator import attrgetter
from random import shuffle from random import shuffle
from typing import List, Optional from typing import List, Optional
import datetime as dt
import re
# PyQt imports
from PyQt6.QtCore import ( from PyQt6.QtCore import (
QAbstractTableModel, QAbstractTableModel,
QModelIndex, QModelIndex,
@ -32,6 +25,11 @@ from PyQt6.QtGui import (
QFont, 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 classes import track_sequence, MusicMusterSignals, PlaylistTrack
from config import Config from config import Config
from helpers import ( from helpers import (
@ -42,7 +40,7 @@ from helpers import (
set_track_metadata, set_track_metadata,
) )
from log import log from log import log
from models import NoteColours, Playdates, PlaylistRows, Tracks from models import db, NoteColours, Playdates, PlaylistRows, Tracks
HEADER_NOTES_COLUMN = 1 HEADER_NOTES_COLUMN = 1
@ -555,7 +553,7 @@ class PlaylistModel(QAbstractTableModel):
else: else:
new_row_number = proposed_row_number 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 return new_row_number
def get_row_info(self, row_number: int) -> PlaylistRowData: def get_row_info(self, row_number: int) -> PlaylistRowData:
@ -753,7 +751,7 @@ class PlaylistModel(QAbstractTableModel):
Insert a row. 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) 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 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 next_start_time: Optional[dt.datetime] = None
update_rows: List[int] = [] 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] prd = self.playlist_rows[row_number]
# Reset start_time if this is the current row # 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]] [[package]]
name = "black" name = "black"
version = "24.2.0" version = "24.3.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"},
{file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"},
{file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"},
{file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"},
{file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"},
{file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"},
{file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"},
{file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"},
{file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"},
{file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"},
{file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"},
{file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"},
{file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"},
{file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"},
{file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"},
{file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"},
{file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"},
{file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"},
{file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"},
{file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"},
{file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"},
{file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"},
] ]
[package.dependencies] [package.dependencies]
@ -265,6 +265,21 @@ files = [
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, {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]] [[package]]
name = "click" name = "click"
version = "8.1.7" version = "8.1.7"
@ -601,23 +616,23 @@ files = [
[[package]] [[package]]
name = "importlib-metadata" name = "importlib-metadata"
version = "7.0.1" version = "7.1.0"
description = "Read metadata from Python packages" description = "Read metadata from Python packages"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"},
{file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"},
] ]
[package.dependencies] [package.dependencies]
zipp = ">=0.5" zipp = ">=0.5"
[package.extras] [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"] 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]] [[package]]
name = "iniconfig" name = "iniconfig"
@ -965,39 +980,39 @@ files = [
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "1.8.0" version = "1.9.0"
description = "Optional static typing for Python" description = "Optional static typing for Python"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"},
{file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"},
{file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"},
{file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"},
{file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"},
{file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"},
{file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"},
{file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"},
{file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"},
{file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"},
{file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"},
{file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"},
{file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"},
{file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"},
{file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"},
{file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"},
{file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"},
{file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"},
{file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"},
{file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"},
{file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"},
{file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"},
{file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"},
{file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"},
{file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"},
{file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"},
{file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"},
] ]
[package.dependencies] [package.dependencies]
@ -1109,14 +1124,14 @@ dev = ["black", "isort", "pytest", "pytest-randomly"]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "23.2" version = "24.0"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
] ]
[[package]] [[package]]
@ -1484,16 +1499,16 @@ PyQt6-sip = ">=13.6,<14"
[[package]] [[package]]
name = "pyqt6-qt6" name = "pyqt6-qt6"
version = "6.6.2" version = "6.6.3"
description = "The subset of a Qt installation needed by PyQt6." description = "The subset of a Qt installation needed by PyQt6."
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "PyQt6_Qt6-6.6.2-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:7ef446d3ffc678a8586ff6dc9f0d27caf4dff05dea02c353540d2f614386faf9"}, {file = "PyQt6_Qt6-6.6.3-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:1674d161ea49a36e9146fd652e789d413a246cc2455ac8bf9c76902b4bd3b986"},
{file = "PyQt6_Qt6-6.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b8363d88623342a72ac17da9127dc12f259bb3148796ea029762aa2d499778d9"}, {file = "PyQt6_Qt6-6.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:18fe1fbbc709dcff5c513e3cac7b1d7b630fb189e6d32a1601f193d73d326f42"},
{file = "PyQt6_Qt6-6.6.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:8d7f674a4ec43ca00191e14945ca4129acbe37a2172ed9d08214ad58b170bc11"}, {file = "PyQt6_Qt6-6.6.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:6ae465dfcbb819dae5e18e8c96abba735b5bb2f16c066497dda4b7ca17c066ce"},
{file = "PyQt6_Qt6-6.6.2-py3-none-win_amd64.whl", hash = "sha256:5a41fe9d53b9e29e9ec5c23f3c5949dba160f90ca313ee8b96b8ffe6a5059387"}, {file = "PyQt6_Qt6-6.6.3-py3-none-win_amd64.whl", hash = "sha256:dbe509eccc579f8818b2b2e8ba93e27986facdd1d4d83ef1c7d9bd47cdf32651"},
] ]
[[package]] [[package]]
@ -1548,32 +1563,32 @@ PyQt6-WebEngine-Qt6 = ">=6.6.0"
[[package]] [[package]]
name = "pyqt6-webengine-qt6" name = "pyqt6-webengine-qt6"
version = "6.6.2" version = "6.6.3"
description = "The subset of a Qt installation needed by PyQt6-WebEngine." description = "The subset of a Qt installation needed by PyQt6-WebEngine."
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ 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.3-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:4ce545accc5a58d62bde7ce18253a70b3970c28a24c94642ec89537352c23974"},
{file = "PyQt6_WebEngine_Qt6-6.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2364dfa3a6e751ead71b7ba759081be677fcf1c6bbd8a2a2a250eb5f06432e8"}, {file = "PyQt6_WebEngine_Qt6-6.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a82308115193a6f220d6310453d1edbe30f1a8ac32c01fc813865319a2199959"},
{file = "PyQt6_WebEngine_Qt6-6.6.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3da4db9ddd984b647d0b79fa10fc6cf65364dfe283cd702b12cb7164be2307cd"}, {file = "PyQt6_WebEngine_Qt6-6.6.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:87f636e23e9c1a1326bf91d273da6bdfed2f42fcc243e527e7b0dbc4f39e70dd"},
{file = "PyQt6_WebEngine_Qt6-6.6.2-py3-none-win_amd64.whl", hash = "sha256:5d6f3ae521115cee77fea22b0248e7b219995390b951b51e4d519aef9c304ca8"}, {file = "PyQt6_WebEngine_Qt6-6.6.3-py3-none-win_amd64.whl", hash = "sha256:3d3e81db62f166f5fbc24b28660fe81c1be4390282bfb9bb48111f32a6bd0f51"},
] ]
[[package]] [[package]]
name = "pyqtgraph" name = "pyqtgraph"
version = "0.13.3" version = "0.13.4"
description = "Scientific Graphics and GUI Library for Python" description = "Scientific Graphics and GUI Library for Python"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.9"
files = [ files = [
{file = "pyqtgraph-0.13.3-py3-none-any.whl", hash = "sha256:fdcc04ac4b32a7bedf1bf3cf74cbb93ab3ba5687791712bbfa8d0712377d2f2b"}, {file = "pyqtgraph-0.13.4-py3-none-any.whl", hash = "sha256:1dc9a786aa43cd787114366058dc3b4b8cb96a0e318f334720c7e6cc6c285940"},
{file = "pyqtgraph-0.13.3.tar.gz", hash = "sha256:58108d8411c7054e0841d8b791ee85e101fc296b9b359c0e01dde38a98ff2ace"}, {file = "pyqtgraph-0.13.4.tar.gz", hash = "sha256:67b0d371405c4fd5f35afecfeb37d4b73bc118f187c52a965ed68d62f59b67b3"},
] ]
[package.dependencies] [package.dependencies]
numpy = ">=1.20.0" numpy = ">=1.22.0"
[[package]] [[package]]
name = "pyreadline3" name = "pyreadline3"
@ -1589,14 +1604,14 @@ files = [
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "7.4.4" version = "8.1.1"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"},
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"},
] ]
[package.dependencies] [package.dependencies]
@ -1604,11 +1619,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*" iniconfig = "*"
packaging = "*" packaging = "*"
pluggy = ">=0.12,<2.0" pluggy = ">=1.4,<2.0"
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras] [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]] [[package]]
name = "pytest-cov" name = "pytest-cov"
@ -1736,19 +1751,19 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "69.1.1" version = "69.2.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages" description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"},
{file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"},
] ]
[package.extras] [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"] 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"] 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]] [[package]]
@ -1763,6 +1778,28 @@ files = [
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, {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]] [[package]]
name = "snowballstemmer" name = "snowballstemmer"
version = "2.2.0" version = "2.2.0"
@ -1943,61 +1980,61 @@ test = ["pytest"]
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "2.0.27" version = "2.0.29"
description = "Database Abstraction Library" description = "Database Abstraction Library"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "SQLAlchemy-2.0.27-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d04e579e911562f1055d26dab1868d3e0bb905db3bccf664ee8ad109f035618a"}, {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c142852ae192e9fe5aad5c350ea6befe9db14370b34047e1f0f7cf99e63c63b"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa67d821c1fd268a5a87922ef4940442513b4e6c377553506b9db3b83beebbd8"}, {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:99a1e69d4e26f71e750e9ad6fdc8614fbddb67cfe2173a3628a2566034e223c7"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c7a596d0be71b7baa037f4ac10d5e057d276f65a9a611c46970f012752ebf2d"}, {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef3fbccb4058355053c51b82fd3501a6e13dd808c8d8cd2561e610c5456013c"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:954d9735ee9c3fa74874c830d089a815b7b48df6f6b6e357a74130e478dbd951"}, {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d6753305936eddc8ed190e006b7bb33a8f50b9854823485eed3a886857ab8d1"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5cd20f58c29bbf2680039ff9f569fa6d21453fbd2fa84dbdb4092f006424c2e6"}, {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0f3ca96af060a5250a8ad5a63699180bc780c2edf8abf96c58af175921df847a"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:03f448ffb731b48323bda68bcc93152f751436ad6037f18a42b7e16af9e91c07"}, {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c4520047006b1d3f0d89e0532978c0688219857eb2fee7c48052560ae76aca1e"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-win32.whl", hash = "sha256:d997c5938a08b5e172c30583ba6b8aad657ed9901fc24caf3a7152eeccb2f1b4"}, {file = "SQLAlchemy-2.0.29-cp310-cp310-win32.whl", hash = "sha256:b2a0e3cf0caac2085ff172c3faacd1e00c376e6884b5bc4dd5b6b84623e29e4f"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-win_amd64.whl", hash = "sha256:eb15ef40b833f5b2f19eeae65d65e191f039e71790dd565c2af2a3783f72262f"}, {file = "SQLAlchemy-2.0.29-cp310-cp310-win_amd64.whl", hash = "sha256:01d10638a37460616708062a40c7b55f73e4d35eaa146781c683e0fa7f6c43fb"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c5bad7c60a392850d2f0fee8f355953abaec878c483dd7c3836e0089f046bf6"}, {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:308ef9cb41d099099fffc9d35781638986870b29f744382904bf9c7dadd08513"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3012ab65ea42de1be81fff5fb28d6db893ef978950afc8130ba707179b4284a"}, {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:296195df68326a48385e7a96e877bc19aa210e485fa381c5246bc0234c36c78e"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbcd77c4d94b23e0753c5ed8deba8c69f331d4fd83f68bfc9db58bc8983f49cd"}, {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a13b917b4ffe5a0a31b83d051d60477819ddf18276852ea68037a144a506efb9"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d177b7e82f6dd5e1aebd24d9c3297c70ce09cd1d5d37b43e53f39514379c029c"}, {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f6d971255d9ddbd3189e2e79d743ff4845c07f0633adfd1de3f63d930dbe673"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:680b9a36029b30cf063698755d277885d4a0eab70a2c7c6e71aab601323cba45"}, {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:61405ea2d563407d316c63a7b5271ae5d274a2a9fbcd01b0aa5503635699fa1e"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1306102f6d9e625cebaca3d4c9c8f10588735ef877f0360b5cdb4fdfd3fd7131"}, {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de7202ffe4d4a8c1e3cde1c03e01c1a3772c92858837e8f3879b497158e4cb44"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-win32.whl", hash = "sha256:5b78aa9f4f68212248aaf8943d84c0ff0f74efc65a661c2fc68b82d498311fd5"}, {file = "SQLAlchemy-2.0.29-cp311-cp311-win32.whl", hash = "sha256:b5d7ed79df55a731749ce65ec20d666d82b185fa4898430b17cb90c892741520"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-win_amd64.whl", hash = "sha256:15e19a84b84528f52a68143439d0c7a3a69befcd4f50b8ef9b7b69d2628ae7c4"}, {file = "SQLAlchemy-2.0.29-cp311-cp311-win_amd64.whl", hash = "sha256:205f5a2b39d7c380cbc3b5dcc8f2762fb5bcb716838e2d26ccbc54330775b003"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0de1263aac858f288a80b2071990f02082c51d88335a1db0d589237a3435fe71"}, {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d96710d834a6fb31e21381c6d7b76ec729bd08c75a25a5184b1089141356171f"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce850db091bf7d2a1f2fdb615220b968aeff3849007b1204bf6e3e50a57b3d32"}, {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52de4736404e53c5c6a91ef2698c01e52333988ebdc218f14c833237a0804f1b"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dfc936870507da96aebb43e664ae3a71a7b96278382bcfe84d277b88e379b18"}, {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c7b02525ede2a164c5fa5014915ba3591730f2cc831f5be9ff3b7fd3e30958e"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4fbe6a766301f2e8a4519f4500fe74ef0a8509a59e07a4085458f26228cd7cc"}, {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dfefdb3e54cd15f5d56fd5ae32f1da2d95d78319c1f6dfb9bcd0eb15d603d5d"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4535c49d961fe9a77392e3a630a626af5baa967172d42732b7a43496c8b28876"}, {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a88913000da9205b13f6f195f0813b6ffd8a0c0c2bd58d499e00a30eb508870c"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0fb3bffc0ced37e5aa4ac2416f56d6d858f46d4da70c09bb731a246e70bff4d5"}, {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fecd5089c4be1bcc37c35e9aa678938d2888845a134dd016de457b942cf5a758"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-win32.whl", hash = "sha256:7f470327d06400a0aa7926b375b8e8c3c31d335e0884f509fe272b3c700a7254"}, {file = "SQLAlchemy-2.0.29-cp312-cp312-win32.whl", hash = "sha256:8197d6f7a3d2b468861ebb4c9f998b9df9e358d6e1cf9c2a01061cb9b6cf4e41"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-win_amd64.whl", hash = "sha256:f9374e270e2553653d710ece397df67db9d19c60d2647bcd35bfc616f1622dcd"}, {file = "SQLAlchemy-2.0.29-cp312-cp312-win_amd64.whl", hash = "sha256:9b19836ccca0d321e237560e475fd99c3d8655d03da80c845c4da20dda31b6e1"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e97cf143d74a7a5a0f143aa34039b4fecf11343eed66538610debc438685db4a"}, {file = "SQLAlchemy-2.0.29-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:87a1d53a5382cdbbf4b7619f107cc862c1b0a4feb29000922db72e5a66a5ffc0"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7b5a3e2120982b8b6bd1d5d99e3025339f7fb8b8267551c679afb39e9c7c7f1"}, {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0732dffe32333211801b28339d2a0babc1971bc90a983e3035e7b0d6f06b93"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e36aa62b765cf9f43a003233a8c2d7ffdeb55bc62eaa0a0380475b228663a38f"}, {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90453597a753322d6aa770c5935887ab1fc49cc4c4fdd436901308383d698b4b"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5ada0438f5b74c3952d916c199367c29ee4d6858edff18eab783b3978d0db16d"}, {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ea311d4ee9a8fa67f139c088ae9f905fcf0277d6cd75c310a21a88bf85e130f5"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b1d9d1bfd96eef3c3faedb73f486c89e44e64e40e5bfec304ee163de01cf996f"}, {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5f20cb0a63a3e0ec4e169aa8890e32b949c8145983afa13a708bc4b0a1f30e03"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-win32.whl", hash = "sha256:ca891af9f3289d24a490a5fde664ea04fe2f4984cd97e26de7442a4251bd4b7c"}, {file = "SQLAlchemy-2.0.29-cp37-cp37m-win32.whl", hash = "sha256:e5bbe55e8552019c6463709b39634a5fc55e080d0827e2a3a11e18eb73f5cdbd"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-win_amd64.whl", hash = "sha256:fd8aafda7cdff03b905d4426b714601c0978725a19efc39f5f207b86d188ba01"}, {file = "SQLAlchemy-2.0.29-cp37-cp37m-win_amd64.whl", hash = "sha256:c2f9c762a2735600654c654bf48dad388b888f8ce387b095806480e6e4ff6907"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec1f5a328464daf7a1e4e385e4f5652dd9b1d12405075ccba1df842f7774b4fc"}, {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e614d7a25a43a9f54fcce4675c12761b248547f3d41b195e8010ca7297c369c"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad862295ad3f644e3c2c0d8b10a988e1600d3123ecb48702d2c0f26771f1c396"}, {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:471fcb39c6adf37f820350c28aac4a7df9d3940c6548b624a642852e727ea586"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48217be1de7d29a5600b5c513f3f7664b21d32e596d69582be0a94e36b8309cb"}, {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:988569c8732f54ad3234cf9c561364221a9e943b78dc7a4aaf35ccc2265f1930"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e56afce6431450442f3ab5973156289bd5ec33dd618941283847c9fd5ff06bf"}, {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dddaae9b81c88083e6437de95c41e86823d150f4ee94bf24e158a4526cbead01"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:611068511b5531304137bcd7fe8117c985d1b828eb86043bd944cebb7fae3910"}, {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:334184d1ab8f4c87f9652b048af3f7abea1c809dfe526fb0435348a6fef3d380"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b86abba762ecfeea359112b2bb4490802b340850bbee1948f785141a5e020de8"}, {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:38b624e5cf02a69b113c8047cf7f66b5dfe4a2ca07ff8b8716da4f1b3ae81567"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-win32.whl", hash = "sha256:30d81cc1192dc693d49d5671cd40cdec596b885b0ce3b72f323888ab1c3863d5"}, {file = "SQLAlchemy-2.0.29-cp38-cp38-win32.whl", hash = "sha256:bab41acf151cd68bc2b466deae5deeb9e8ae9c50ad113444151ad965d5bf685b"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-win_amd64.whl", hash = "sha256:120af1e49d614d2525ac247f6123841589b029c318b9afbfc9e2b70e22e1827d"}, {file = "SQLAlchemy-2.0.29-cp38-cp38-win_amd64.whl", hash = "sha256:52c8011088305476691b8750c60e03b87910a123cfd9ad48576d6414b6ec2a1d"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d07ee7793f2aeb9b80ec8ceb96bc8cc08a2aec8a1b152da1955d64e4825fcbac"}, {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3071ad498896907a5ef756206b9dc750f8e57352113c19272bdfdc429c7bd7de"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb0845e934647232b6ff5150df37ceffd0b67b754b9fdbb095233deebcddbd4a"}, {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dba622396a3170974f81bad49aacebd243455ec3cc70615aeaef9e9613b5bca5"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fc19ae2e07a067663dd24fca55f8ed06a288384f0e6e3910420bf4b1270cc51"}, {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b184e3de58009cc0bf32e20f137f1ec75a32470f5fede06c58f6c355ed42a72"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b90053be91973a6fb6020a6e44382c97739736a5a9d74e08cc29b196639eb979"}, {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c37f1050feb91f3d6c32f864d8e114ff5545a4a7afe56778d76a9aec62638ba"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2f5c9dfb0b9ab5e3a8a00249534bdd838d943ec4cfb9abe176a6c33408430230"}, {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bda7ce59b06d0f09afe22c56714c65c957b1068dee3d5e74d743edec7daba552"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33e8bde8fff203de50399b9039c4e14e42d4d227759155c21f8da4a47fc8053c"}, {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:25664e18bef6dc45015b08f99c63952a53a0a61f61f2e48a9e70cec27e55f699"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-win32.whl", hash = "sha256:d873c21b356bfaf1589b89090a4011e6532582b3a8ea568a00e0c3aab09399dd"}, {file = "SQLAlchemy-2.0.29-cp39-cp39-win32.whl", hash = "sha256:77d29cb6c34b14af8a484e831ab530c0f7188f8efed1c6a833a2c674bf3c26ec"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-win_amd64.whl", hash = "sha256:ff2f1b7c963961d41403b650842dc2039175b906ab2093635d8319bef0b7d620"}, {file = "SQLAlchemy-2.0.29-cp39-cp39-win_amd64.whl", hash = "sha256:04c487305ab035a9548f573763915189fc0fe0824d9ba28433196f8436f1449c"},
{file = "SQLAlchemy-2.0.27-py3-none-any.whl", hash = "sha256:1ab4e0448018d01b142c916cc7119ca573803a4745cfe341b8f95657812700ac"}, {file = "SQLAlchemy-2.0.29-py3-none-any.whl", hash = "sha256:dc4ee2d4ee43251905f88637d5281a8d52e916a021384ec10758826f5cbae305"},
{file = "SQLAlchemy-2.0.27.tar.gz", hash = "sha256:86a6ed69a71fe6b88bf9331594fa390a2adda4a49b5c06f98e47bf0d392534f8"}, {file = "SQLAlchemy-2.0.29.tar.gz", hash = "sha256:bd9566b8e58cabd700bc367b60e90d9349cd16f0984973f98a9a09f9c64e86f0"},
] ]
[package.dependencies] [package.dependencies]
@ -2051,14 +2088,14 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
[[package]] [[package]]
name = "stackprinter" name = "stackprinter"
version = "0.2.11" version = "0.2.12"
description = "Debug-friendly stack traces, with variable values and semantic highlighting" description = "Debug-friendly stack traces, with variable values and semantic highlighting"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.4" python-versions = ">=3.4"
files = [ files = [
{file = "stackprinter-0.2.11-py3-none-any.whl", hash = "sha256:101da55db7dfd54af516e3e209db9c84645285e5ea00d0b0709418dde2f157a1"}, {file = "stackprinter-0.2.12-py3-none-any.whl", hash = "sha256:0a0623d46a5babd7a8a9787f605f4dd4a42d6ff7aee140541d5e9291a506e8d9"},
{file = "stackprinter-0.2.11.tar.gz", hash = "sha256:abbd8f4f892f24a5bd370119af49c3e3408b0bf04cd4d28e99f81c4e781a767b"}, {file = "stackprinter-0.2.12.tar.gz", hash = "sha256:271efc75ebdcc1554e58168ea7779f98066d54a325f57c7dc19f10fa998ef01e"},
] ]
[[package]] [[package]]
@ -2144,30 +2181,30 @@ files = [
[[package]] [[package]]
name = "traitlets" name = "traitlets"
version = "5.14.1" version = "5.14.2"
description = "Traitlets Python configuration system" description = "Traitlets Python configuration system"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"}, {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"},
{file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"}, {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"},
] ]
[package.extras] [package.extras]
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 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]] [[package]]
name = "types-psutil" name = "types-psutil"
version = "5.9.5.20240205" version = "5.9.5.20240316"
description = "Typing stubs for psutil" description = "Typing stubs for psutil"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "types-psutil-5.9.5.20240205.tar.gz", hash = "sha256:51df36a361aa597bf483dcc5b58f2ab7aa87452a36d2da97c90994d6a81ef743"}, {file = "types-psutil-5.9.5.20240316.tar.gz", hash = "sha256:5636f5714bb930c64bb34c4d47a59dc92f9d610b778b5364a31daa5584944848"},
{file = "types_psutil-5.9.5.20240205-py3-none-any.whl", hash = "sha256:3ec9bd8b95a64fe1269241d3ffb74b94a45df2d0391da1402423cd33f29745ca"}, {file = "types_psutil-5.9.5.20240316-py3-none-any.whl", hash = "sha256:2fdd64ea6e97befa546938f486732624f9255fde198b55e6f00fda236f059f64"},
] ]
[[package]] [[package]]
@ -2202,14 +2239,14 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "urwid" name = "urwid"
version = "2.6.7" version = "2.6.10"
description = "A full-featured console (xterm et al.) user interface library" description = "A full-featured console (xterm et al.) user interface library"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">3.7" python-versions = ">3.7"
files = [ files = [
{file = "urwid-2.6.7-py3-none-any.whl", hash = "sha256:80b922d2051db6abe598b7e1b0b31d8d04fcc56d35bb1ec40b3c128fa0bd23ab"}, {file = "urwid-2.6.10-py3-none-any.whl", hash = "sha256:f5d290ab01a9cf69a062d5d04ff69111903d41fc14ed03f3ed92cb36f5ef4735"},
{file = "urwid-2.6.7.tar.gz", hash = "sha256:597fa2d19ac788e4607d2a48aca32f257342201cb55e5f6a00a8fcd24e62a5ab"}, {file = "urwid-2.6.10.tar.gz", hash = "sha256:ae33355c414c13214e541d3634f3c8a0bfb373914e62ffbcf2fa863527706321"},
] ]
[package.dependencies] [package.dependencies]
@ -2274,21 +2311,21 @@ test = ["websockets"]
[[package]] [[package]]
name = "zipp" name = "zipp"
version = "3.17.0" version = "3.18.1"
description = "Backport of pathlib-compatible object wrapper for zip files" description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"},
{file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"},
] ]
[package.extras] [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"]
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"] 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] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "f4fb2696ae984283c4c0d7816ba7cbd7be714695d6eb3c84b5da62b3809f9c82" content-hash = "e8a4a3f4b5dd70bd5fb2ab420b4de6e3304a15be383233bb01b966e047700cd1"

View File

@ -31,7 +31,6 @@ alchemical = "^1.0.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
ipdb = "^0.13.9" ipdb = "^0.13.9"
pytest = "^7.0.1"
pytest-qt = "^4.0.2" pytest-qt = "^4.0.2"
pydub-stubs = "^0.25.1" pydub-stubs = "^0.25.1"
line-profiler = "^4.0.2" line-profiler = "^4.0.2"
@ -46,6 +45,8 @@ flakehell = "^0.9.0"
mypy = "^1.7.0" mypy = "^1.7.0"
pdbp = "^1.5.0" pdbp = "^1.5.0"
pytest-cov = "^5.0.0" pytest-cov = "^5.0.0"
pytest = "^8.1.1"
snoop = "^0.4.3"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]

View File

@ -1,4 +1,12 @@
# Standard library imports
import datetime as dt import datetime as dt
import unittest
# PyQt imports
# Third party imports
# App imports
from helpers import ( from helpers import (
fade_point, fade_point,
get_audio_segment, get_audio_segment,
@ -9,7 +17,14 @@ from helpers import (
) )
def test_fade_point(): class TestMMHelpers(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_fade_point(self):
test_track_path = "testdata/isa.mp3" test_track_path = "testdata/isa.mp3"
test_track_data = "testdata/isa.py" test_track_data = "testdata/isa.py"
@ -26,8 +41,7 @@ def test_fade_point():
assert fade_at < testdata["fade_at"] + 1000 assert fade_at < testdata["fade_at"] + 1000
assert fade_at > testdata["fade_at"] - 1000 assert fade_at > testdata["fade_at"] - 1000
def test_get_tags(self):
def test_get_tags():
test_track_path = "testdata/mom.mp3" test_track_path = "testdata/mom.mp3"
test_track_data = "testdata/mom.py" test_track_data = "testdata/mom.py"
@ -40,8 +54,7 @@ def test_get_tags():
assert tags["artist"] == testdata["artist"] assert tags["artist"] == testdata["artist"]
assert tags["title"] == testdata["title"] assert tags["title"] == testdata["title"]
def test_get_relative_date(self):
def test_get_relative_date():
assert get_relative_date(None) == "Never" assert get_relative_date(None) == "Never"
today_at_10 = dt.datetime.now().replace(hour=10, minute=0) today_at_10 = dt.datetime.now().replace(hour=10, minute=0)
today_at_11 = dt.datetime.now().replace(hour=11, minute=0) today_at_11 = dt.datetime.now().replace(hour=11, minute=0)
@ -51,8 +64,7 @@ def test_get_relative_date():
sixteen_days_ago = today_at_10 - dt.timedelta(days=16) sixteen_days_ago = today_at_10 - dt.timedelta(days=16)
assert get_relative_date(sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago" assert get_relative_date(sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago"
def test_leading_silence(self):
def test_leading_silence():
test_track_path = "testdata/isa.mp3" test_track_path = "testdata/isa.mp3"
test_track_data = "testdata/isa.py" test_track_data = "testdata/isa.py"
@ -69,8 +81,7 @@ def test_leading_silence():
assert silence_at < testdata["leading_silence"] + 1000 assert silence_at < testdata["leading_silence"] + 1000
assert silence_at > testdata["leading_silence"] - 1000 assert silence_at > testdata["leading_silence"] - 1000
def test_ms_to_mmss(self):
def test_ms_to_mmss():
assert ms_to_mmss(None) == "-" assert ms_to_mmss(None) == "-"
assert ms_to_mmss(59600) == "0:59" assert ms_to_mmss(59600) == "0:59"
assert ms_to_mmss((5 * 60 * 1000) + 23000) == "5:23" assert ms_to_mmss((5 * 60 * 1000) + 23000) == "5:23"

View File

@ -1,22 +1,47 @@
# Standard library imports
import os
import unittest
# PyQt imports
# Third party imports
import pytest 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(): class TestMMMisc(unittest.TestCase):
def setUp(self):
db.create_all()
def tearDown(self):
db.drop_all()
def test_log_exception(self):
"""Test deliberate exception""" """Test deliberate exception"""
with pytest.raises(Exception): with pytest.raises(Exception):
1 / 0 1 / 0
def test_create_settings(self):
def test_create_settings(session):
SETTING_NAME = "wombat" SETTING_NAME = "wombat"
NO_SUCH_SETTING = "abc" NO_SUCH_SETTING = "abc"
VALUE = 3 VALUE = 3
with db.Session() as session:
setting = Settings(session, SETTING_NAME) setting = Settings(session, SETTING_NAME)
# test repr
_ = str(setting)
setting.update(session, dict(f_int=VALUE)) setting.update(session, dict(f_int=VALUE))
print(setting)
_ = Settings.all_as_dict(session) _ = Settings.all_as_dict(session)
test = Settings.get_int_settings(session, SETTING_NAME) test = Settings.get_int_settings(session, SETTING_NAME)
assert test.name == SETTING_NAME assert test.name == SETTING_NAME

View File

@ -1,6 +1,23 @@
# Standard library imports
import datetime as dt 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, Carts,
NoteColours, NoteColours,
Playdates, Playdates,
@ -10,11 +27,29 @@ from app.models import (
) )
def test_notecolours_get_colour(session): 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""" """Create a colour record and retrieve all colours"""
print(">>>text_notcolours_get_colour")
note_colour = "#0bcdef" note_colour = "#0bcdef"
with db.Session() as session:
NoteColours(session, substring="substring", colour=note_colour) NoteColours(session, substring="substring", colour=note_colour)
records = NoteColours.get_all(session) records = NoteColours.get_all(session)
@ -22,13 +57,12 @@ def test_notecolours_get_colour(session):
record = records[0] record = records[0]
assert record.colour == note_colour assert record.colour == note_colour
def test_notecolours_get_all(self):
def test_notecolours_get_all(session):
"""Create two colour records and retrieve them all""" """Create two colour records and retrieve them all"""
print(">>>text_notcolours_get_all")
note1_colour = "#1bcdef" note1_colour = "#1bcdef"
note2_colour = "#20ff00" note2_colour = "#20ff00"
with db.Session() as session:
NoteColours(session, substring="note1", colour=note1_colour) NoteColours(session, substring="note1", colour=note1_colour)
NoteColours(session, substring="note2", colour=note2_colour) NoteColours(session, substring="note2", colour=note2_colour)
@ -37,50 +71,54 @@ def test_notecolours_get_all(session):
assert note1_colour in [n.colour for n in records] assert note1_colour in [n.colour for n in records]
assert note2_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):
def test_notecolours_get_colour_none(session):
note_colour = "#3bcdef" note_colour = "#3bcdef"
with db.Session() as session:
NoteColours(session, substring="substring", colour=note_colour) NoteColours(session, substring="substring", colour=note_colour)
result = NoteColours.get_colour(session, "xyz") result = NoteColours.get_colour(session, "xyz")
assert result is None assert result is None
def test_notecolours_get_colour_match(self):
def test_notecolours_get_colour_match(session):
note_colour = "#4bcdef" note_colour = "#4bcdef"
with db.Session() as session:
nc = NoteColours(session, substring="sub", colour=note_colour) nc = NoteColours(session, substring="sub", colour=note_colour)
assert nc assert nc
result = NoteColours.get_colour(session, "The substring") result = NoteColours.get_colour(session, "The substring")
assert result == note_colour assert result == note_colour
def test_playdates_add_playdate(self):
def test_playdates_add_playdate(session, track1):
"""Test playdate and last_played retrieval""" """Test playdate and last_played retrieval"""
playdate = Playdates(session, track1.id) with db.Session() as session:
session.add(self.track1)
playdate = Playdates(session, self.track1.id)
assert playdate assert playdate
print(playdate) # test repr
_ = str(playdate)
last_played = Playdates.last_played(session, track1.id) last_played = Playdates.last_played(session, self.track1.id)
assert abs((playdate.lastplayed - last_played).total_seconds()) < 2 assert abs((playdate.lastplayed - last_played).total_seconds()) < 2
def test_playdates_played_after(self):
def test_playdates_played_after(session, track1): with db.Session() as session:
playdate = Playdates(session, track1.id) session.add(self.track1)
playdate = Playdates(session, self.track1.id)
yesterday = dt.datetime.now() - dt.timedelta(days=1) yesterday = dt.datetime.now() - dt.timedelta(days=1)
played = Playdates.played_after(session, yesterday) played = Playdates.played_after(session, yesterday)
assert len(played) == 1 assert len(played) == 1
assert played[0] == playdate assert played[0] == playdate
def test_playlist_create(self):
def test_playlist_create(session):
TEMPLATE_NAME = "my template" TEMPLATE_NAME = "my template"
with db.Session() as session:
playlist = Playlists(session, "my playlist") playlist = Playlists(session, "my playlist")
assert playlist assert playlist
print(playlist) # test repr
_ = str(playlist)
# test clear tabs # test clear tabs
Playlists.clear_tabs(session, [playlist.id]) Playlists.clear_tabs(session, [playlist.id])
@ -89,7 +127,9 @@ def test_playlist_create(session):
Playlists.save_as_template(session, playlist.id, TEMPLATE_NAME) Playlists.save_as_template(session, playlist.id, TEMPLATE_NAME)
# test create template # test create template
_ = Playlists.create_playlist_from_template(session, playlist, "my new name") _ = Playlists.create_playlist_from_template(
session, playlist, "my new name"
)
# get all templates # get all templates
all_templates = Playlists.get_all_templates(session) all_templates = Playlists.get_all_templates(session)
@ -99,9 +139,9 @@ def test_playlist_create(session):
# test delete playlist # test delete playlist
playlist.delete(session) playlist.delete(session)
def test_playlist_open_and_close(self):
def test_playlist_open_and_close(session):
# We need a playlist # We need a playlist
with db.Session() as session:
playlist = Playlists(session, "my playlist") playlist = Playlists(session, "my playlist")
assert len(Playlists.get_open(session)) == 0 assert len(Playlists.get_open(session)) == 0
@ -117,11 +157,11 @@ def test_playlist_open_and_close(session):
assert len(Playlists.get_open(session)) == 0 assert len(Playlists.get_open(session)) == 0
assert len(Playlists.get_closed(session)) == 1 assert len(Playlists.get_closed(session)) == 1
def test_playlist_get_all_and_by_id(self):
def test_playlist_get_all_and_by_id(session):
# We need two playlists # We need two playlists
p1_name = "playlist one" p1_name = "playlist one"
p2_name = "playlist two" p2_name = "playlist two"
with db.Session() as session:
playlist1 = Playlists(session, p1_name) playlist1 = Playlists(session, p1_name)
_ = Playlists(session, p2_name) _ = Playlists(session, p2_name)
@ -131,44 +171,48 @@ def test_playlist_get_all_and_by_id(session):
assert p2_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 assert session.get(Playlists, playlist1.id).name == p1_name
def test_tracks_get_all_tracks(self):
def test_tracks_get_all_tracks(session, track1, track2):
# Need two tracks # 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)] result = [a.path for a in Tracks.get_all(session)]
assert track1.path in result assert self.track1.path in result
assert track2.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_path(session, 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
assert Tracks.get_by_path(session, track1.path) is track1 def test_tracks_search_artists(self):
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" track1_artist = "Fleetwood Mac"
with db.Session() as session:
session.add(self.track1)
assert len(Tracks.search_artists(session, track1_artist)) == 1 assert len(Tracks.search_artists(session, track1_artist)) == 1
def test_tracks_search_titles(self):
def test_tracks_search_titles(session, track1):
track1_title = "I'm So Afraid" track1_title = "I'm So Afraid"
with db.Session() as session:
session.add(self.track1)
assert len(Tracks.search_titles(session, track1_title)) == 1 assert len(Tracks.search_titles(session, track1_title)) == 1
def test_repr(session): def test_repr(self):
"""Just check for error retrieving reprs""" """Just check for error retrieving reprs"""
with db.Session() as session:
nc = NoteColours(session, substring="x", colour="x") nc = NoteColours(session, substring="x", colour="x")
print(nc) _ = str(nc)
def test_get_colour(self):
def test_get_colour(session):
"""Test for errors in execution""" """Test for errors in execution"""
GOOD_STRING = "cantelope" GOOD_STRING = "cantelope"
@ -176,7 +220,10 @@ def test_get_colour(session):
SUBSTR = "ant" SUBSTR = "ant"
COLOUR = "blue" COLOUR = "blue"
nc1 = NoteColours(session, substring=SUBSTR, colour=COLOUR, is_casesensitive=True) with db.Session() as session:
nc1 = NoteColours(
session, substring=SUBSTR, colour=COLOUR, is_casesensitive=True
)
_ = nc1.get_colour(session, "") _ = nc1.get_colour(session, "")
colour = nc1.get_colour(session, GOOD_STRING) colour = nc1.get_colour(session, GOOD_STRING)
@ -185,7 +232,9 @@ def test_get_colour(session):
colour = nc1.get_colour(session, BAD_STRING) colour = nc1.get_colour(session, BAD_STRING)
assert colour is None assert colour is None
nc2 = NoteColours(session, substring=".*" + SUBSTR, colour=COLOUR, is_regex=True) nc2 = NoteColours(
session, substring=".*" + SUBSTR, colour=COLOUR, is_regex=True
)
colour = nc2.get_colour(session, GOOD_STRING) colour = nc2.get_colour(session, GOOD_STRING)
assert colour == COLOUR assert colour == COLOUR
@ -193,7 +242,11 @@ def test_get_colour(session):
assert colour is None assert colour is None
nc3 = NoteColours( nc3 = NoteColours(
session, substring=".*" + SUBSTR, colour=COLOUR, is_regex=True, is_casesensitive=True session,
substring=".*" + SUBSTR,
colour=COLOUR,
is_regex=True,
is_casesensitive=True,
) )
colour = nc3.get_colour(session, GOOD_STRING) colour = nc3.get_colour(session, GOOD_STRING)
@ -202,17 +255,17 @@ def test_get_colour(session):
colour = nc3.get_colour(session, BAD_STRING) colour = nc3.get_colour(session, BAD_STRING)
assert colour is None assert colour is None
def test_create_cart(self):
def test_create_cart(session): with db.Session() as session:
cart = Carts(session, 1, "name") cart = Carts(session, 1, "name")
assert cart assert cart
print(cart) _ = str(cart)
def test_name_available(self):
def test_name_available(session):
PLAYLIST_NAME = "a name" PLAYLIST_NAME = "a name"
RENAME = "new name" RENAME = "new name"
with db.Session() as session:
if Playlists.name_is_available(session, PLAYLIST_NAME): if Playlists.name_is_available(session, PLAYLIST_NAME):
playlist = Playlists(session, PLAYLIST_NAME) playlist = Playlists(session, PLAYLIST_NAME)
assert playlist assert playlist
@ -221,23 +274,23 @@ def test_name_available(session):
playlist.rename(session, RENAME) playlist.rename(session, RENAME)
def test_create_playlist_row(self):
def test_create_playlist_row(session):
PLAYLIST_NAME = "a name" PLAYLIST_NAME = "a name"
with db.Session() as session:
if Playlists.name_is_available(session, PLAYLIST_NAME): if Playlists.name_is_available(session, PLAYLIST_NAME):
playlist = Playlists(session, PLAYLIST_NAME) playlist = Playlists(session, PLAYLIST_NAME)
plr = PlaylistRows(session, playlist.id, 1) plr = PlaylistRows(session, playlist.id, 1)
assert plr assert plr
print(plr) _ = str(plr)
plr.append_note("a note") plr.append_note("a note")
plr.append_note("another note") plr.append_note("another note")
def test_delete_plr(self):
def test_delete_plr(session):
PLAYLIST_NAME = "a name" PLAYLIST_NAME = "a name"
with db.Session() as session:
if Playlists.name_is_available(session, PLAYLIST_NAME): if Playlists.name_is_available(session, PLAYLIST_NAME):
playlist = Playlists(session, PLAYLIST_NAME) playlist = Playlists(session, PLAYLIST_NAME)
@ -246,5 +299,3 @@ def test_delete_plr(session):
PlaylistRows.delete_higher_rows(session, plr.playlist_id, 10) PlaylistRows.delete_higher_rows(session, plr.playlist_id, 10)
assert PlaylistRows.get_track_plr(session, 12, plr.playlist_id) is None assert PlaylistRows.get_track_plr(session, 12, plr.playlist_id) is None

View File

@ -1,16 +1,36 @@
# Standard library imports
import os
import unittest
from typing import Optional from typing import Optional
from app.models import ( # PyQt imports
Playlists,
Tracks,
)
from PyQt6.QtCore import Qt, QModelIndex from PyQt6.QtCore import Qt, QModelIndex
from app.helpers import get_file_metadata # Third party imports
from app import playlistmodel from sqlalchemy.orm.session import Session
from dbconfig import scoped_session
test_tracks = [ # App imports
from app.log import log
from app.helpers import get_file_metadata
# Set up test database before importing db
# Mark subsequent lines to ignore E402, imports not at top of file
DB_FILE = '/tmp/mm.db'
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.mp3",
"testdata/isa_with_gap.mp3", "testdata/isa_with_gap.mp3",
"testdata/loser.mp3", "testdata/loser.mp3",
@ -20,361 +40,184 @@ test_tracks = [
"testdata/sitting.mp3", "testdata/sitting.mp3",
] ]
db.create_all()
def create_model_with_tracks(session: scoped_session, name: Optional[str] = None) -> "playlistmodel.PlaylistModel": # Create a playlist and model
playlist = Playlists(session, name or "test playlist") with db.Session() as session:
model = playlistmodel.PlaylistModel(playlist.id) self.playlist = Playlists(session, PLAYLIST_NAME)
self.model = playlistmodel.PlaylistModel(self.playlist.id)
for row in range(len(test_tracks)): for row in range(len(self.test_tracks)):
track_path = test_tracks[row % len(test_tracks)] track_path = self.test_tracks[row % len(self.test_tracks)]
metadata = get_file_metadata(track_path) metadata = get_file_metadata(track_path)
track = Tracks(session, **metadata) track = Tracks(session, **metadata)
model.insert_row(proposed_row_number=row, track_id=track.id, note=f"{row=}") self.model.insert_row(proposed_row_number=row, track_id=track.id, note=f"{row=}")
session.commit() 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): def tearDown(self):
# move row 3 to row 5 db.drop_all()
monkeypatch.setattr(playlistmodel, "Session", session)
model = create_model_with_playlist_rows(session, 11) def test_7_row_playlist(self):
model.move_rows([3], 5) # Test auto-created playlist
# Check we have all rows and plr_rownums are correct
for row in range(model.rowCount()): assert self.model.rowCount() == 7
assert row in model.playlist_rows assert max(self.model.playlist_rows.keys()) == 6
assert model.playlist_rows[row].plr_rownum == row for row in range(self.model.rowCount()):
if row not in [3, 4, 5]: assert row in self.model.playlist_rows
assert model.playlist_rows[row].note == str(row) assert self.model.playlist_rows[row].plr_rownum == 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): # def test_move_rows_test2(monkeypatch, session):
# move row 4 to row 3 # # 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)
monkeypatch.setattr(playlistmodel, "Session", session)
model = create_model_with_playlist_rows(session, 11) # def test_move_rows_test3(monkeypatch, session):
model.move_rows([4], 3) # # move row 4 to row 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)
# monkeypatch.setattr(playlistmodel, "Session", session)
def test_move_rows_test4(monkeypatch, session): # model = create_model_with_playlist_rows(session, 11)
# move row 4 to row 2 # model.move_rows([4], 3)
monkeypatch.setattr(playlistmodel, "Session", session)
model = create_model_with_playlist_rows(session, 11)
model.move_rows([4], 2)
# Check we have all rows and plr_rownums are correct
for row in range(model.rowCount()):
assert row in model.playlist_rows
assert model.playlist_rows[row].plr_rownum == row
if row not in [2, 3, 4]:
assert model.playlist_rows[row].note == str(row)
elif row == 2:
assert model.playlist_rows[row].note == str(4)
elif row == 3:
assert model.playlist_rows[row].note == str(2)
elif row == 4:
assert model.playlist_rows[row].note == str(3)
def test_move_rows_test5(monkeypatch, session):
# move rows [1, 4, 5, 10] → 8
monkeypatch.setattr(playlistmodel, "Session", session)
model = create_model_with_playlist_rows(session, 11)
model.move_rows([1, 4, 5, 10], 8)
# Check we have all rows and plr_rownums are correct
new_order = []
for row in range(model.rowCount()):
assert row in model.playlist_rows
assert model.playlist_rows[row].plr_rownum == row
new_order.append(int(model.playlist_rows[row].note))
assert new_order == [0, 2, 3, 6, 7, 8, 9, 1, 4, 5, 10]
def test_move_rows_test6(monkeypatch, session):
# move rows [3, 6] → 5
monkeypatch.setattr(playlistmodel, "Session", session)
model = create_model_with_playlist_rows(session, 11)
model.move_rows([3, 6], 5)
# Check we have all rows and plr_rownums are correct
new_order = []
for row in range(model.rowCount()):
assert row in model.playlist_rows
assert model.playlist_rows[row].plr_rownum == row
new_order.append(int(model.playlist_rows[row].note))
assert new_order == [0, 1, 2, 4, 5, 3, 6, 7, 8, 9, 10]
def test_move_rows_test7(monkeypatch, session): # # Check we have all rows and plr_rownums are correct
# move rows [3, 5, 6] → 8 # 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)
monkeypatch.setattr(playlistmodel, "Session", session)
model = create_model_with_playlist_rows(session, 11) # def test_move_rows_test4(monkeypatch, session):
model.move_rows([3, 5, 6], 8) # # move row 4 to row 2
# Check we have all rows and plr_rownums are correct # monkeypatch.setattr(playlistmodel, "Session", session)
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]
# model = create_model_with_playlist_rows(session, 11)
# model.move_rows([4], 2)
def test_move_rows_test8(monkeypatch, session): # # Check we have all rows and plr_rownums are correct
# move rows [7, 8, 10] → 5 # 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)
monkeypatch.setattr(playlistmodel, "Session", session)
model = create_model_with_playlist_rows(session, 11) # def test_move_rows_test5(monkeypatch, session):
model.move_rows([7, 8, 10], 5) # # move rows [1, 4, 5, 10] → 8
# Check we have all rows and plr_rownums are correct # monkeypatch.setattr(playlistmodel, "Session", session)
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]
# model = create_model_with_playlist_rows(session, 11)
# model.move_rows([1, 4, 5, 10], 8)
def test_insert_header_row_end(monkeypatch, session): # # Check we have all rows and plr_rownums are correct
# insert header row at end of playlist # 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)
note_text = "test text"
initial_row_count = 11
model = create_model_with_playlist_rows(session, initial_row_count) # def test_move_rows_test6(monkeypatch, session):
model.insert_row(proposed_row_number=None, note=note_text) # # move rows [3, 6] → 5
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
)
# monkeypatch.setattr(playlistmodel, "Session", session)
def test_insert_header_row_middle(monkeypatch, session): # model = create_model_with_playlist_rows(session, 11)
# insert header row in middle of playlist # model.move_rows([3, 6], 5)
monkeypatch.setattr(playlistmodel, "Session", session) # # Check we have all rows and plr_rownums are correct
note_text = "test text" # new_order = []
initial_row_count = 11 # for row in range(model.rowCount()):
insert_row = 6 # 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
model = create_model_with_playlist_rows(session, initial_row_count) # monkeypatch.setattr(playlistmodel, "Session", session)
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, 11)
# model.move_rows([3, 5, 6], 8)
# # Check we have all rows and plr_rownums are correct
# new_order = []
# for row in range(model.rowCount()):
# assert row in model.playlist_rows
# assert model.playlist_rows[row].plr_rownum == row
# new_order.append(int(model.playlist_rows[row].note))
# assert new_order == [0, 1, 2, 4, 7, 8, 9, 10, 3, 5, 6]
def test_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) # def test_move_rows_test8(monkeypatch, session):
model.insert_row(proposed_row_number=insert_row, note=note_text) # # move rows [7, 8, 10] → 5
assert model.rowCount() == initial_row_count + 1
prd = model.playlist_rows[1] # monkeypatch.setattr(playlistmodel, "Session", session)
model.add_track_to_header(insert_row, prd.track_id)
# model = create_model_with_playlist_rows(session, 11)
# model.move_rows([7, 8, 10], 5)
def test_create_model_with_tracks(monkeypatch, session): # # Check we have all rows and plr_rownums are correct
monkeypatch.setattr(playlistmodel, "Session", session) # new_order = []
model = create_model_with_tracks(session) # for row in range(model.rowCount()):
assert len(model.playlist_rows) == len(test_tracks) # 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_timing_one_track(monkeypatch, session): # def test_insert_header_row_end(monkeypatch, session):
START_ROW = 0 # # insert header row at end of playlist
END_ROW = 2
monkeypatch.setattr(playlistmodel, "Session", session) # monkeypatch.setattr(playlistmodel, "Session", session)
model = create_model_with_tracks(session) # note_text = "test text"
# initial_row_count = 11
model.insert_row(proposed_row_number=START_ROW, note="start+") # model = create_model_with_playlist_rows(session, initial_row_count)
model.insert_row(proposed_row_number=END_ROW, note="-") # 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
# )
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_header_row_middle(monkeypatch, session):
def test_insert_track_new_playlist(monkeypatch, session): # # insert header row in middle of playlist
# 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
# monkeypatch.setattr(playlistmodel, "Session", session) # monkeypatch.setattr(playlistmodel, "Session", session)
# note_text = "test text" # note_text = "test text"
@ -382,12 +225,188 @@ def test_move_multiple_rows_between_playlists_to_end(monkeypatch, session):
# insert_row = 6 # insert_row = 6
# model = create_model_with_playlist_rows(session, initial_row_count) # 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 # assert model.rowCount() == initial_row_count + 1
# prd = model.playlist_rows[insert_row] # prd = model.playlist_rows[insert_row]
# # Test against edit_role because display_role for headers is # # Test against edit_role because display_role for headers is
# # handled differently (sets up row span) # # handled differently (sets up row span)
# assert ( # assert (
# model.edit_role(model.rowCount(), playlistmodel.Col.NOTE.value, prd) # model.edit_role(model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd)
# == note_text # == 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
# # )