From b0e9f13e11521e694881c49720f7febc82d3e22a Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Sun, 6 Apr 2025 22:28:32 -0400 Subject: [PATCH 01/14] [music] Attempts to fix bad lookups from LastFM and Jellyfin Broader issue was creating tracks without albums that were duplicates of existing tracks because sometimes Jellyfin and LastFM do not have albums sent with them. --- vrobbler/apps/music/lastfm.py | 22 +- .../music/migrations/0025_artist_alt_names.py | 18 ++ .../music/migrations/0026_album_alt_names.py | 18 ++ vrobbler/apps/music/models.py | 255 +++++++++++++++--- vrobbler/apps/music/utils.py | 54 ++-- vrobbler/apps/scrobbles/mixins.py | 6 +- vrobbler/apps/scrobbles/scrobblers.py | 16 +- vrobbler/apps/scrobbles/tsv.py | 33 +-- 8 files changed, 323 insertions(+), 99 deletions(-) create mode 100644 vrobbler/apps/music/migrations/0025_artist_alt_names.py create mode 100644 vrobbler/apps/music/migrations/0026_album_alt_names.py diff --git a/vrobbler/apps/music/lastfm.py b/vrobbler/apps/music/lastfm.py index 3cacb8a..30dc07b 100644 --- a/vrobbler/apps/music/lastfm.py +++ b/vrobbler/apps/music/lastfm.py @@ -1,14 +1,10 @@ import logging -import time -from datetime import datetime, timedelta, UTC +from datetime import datetime, timedelta import pylast import pytz from django.conf import settings -from django.utils import timezone -from music.utils import ( - get_or_create_track, -) +from music.models import Track logger = logging.getLogger(__name__) @@ -47,14 +43,10 @@ class LastFM: lastfm_scrobbles = self.get_last_scrobbles(time_from=last_processed) for lfm_scrobble in lastfm_scrobbles: - track = get_or_create_track( - lfm_scrobble, - { - "TRACK_TITLE": "title", - "ARTIST_NAME": "artist", - "ALBUM_NAME": "album", - "RUN_TIME": "run_time_seconds", - }, + track = Track.find_or_create( + title=lfm_scrobble.get("title"), + artist_name=lfm_scrobble.get("artist"), + album_name=lfm_scrobble.get("album"), ) timezone = settings.TIME_ZONE @@ -149,7 +141,7 @@ class LastFM: continue # TODO figure out if this will actually work - #timestamp = datetime.fromtimestamp(int(scrobble.timestamp), UTC) + # timestamp = datetime.fromtimestamp(int(scrobble.timestamp), UTC) timestamp = datetime.utcfromtimestamp( int(scrobble.timestamp) ).replace(tzinfo=pytz.utc) diff --git a/vrobbler/apps/music/migrations/0025_artist_alt_names.py b/vrobbler/apps/music/migrations/0025_artist_alt_names.py new file mode 100644 index 0000000..d148c71 --- /dev/null +++ b/vrobbler/apps/music/migrations/0025_artist_alt_names.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.19 on 2025-04-07 00:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("music", "0024_alter_track_run_time_seconds"), + ] + + operations = [ + migrations.AddField( + model_name="artist", + name="alt_names", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/vrobbler/apps/music/migrations/0026_album_alt_names.py b/vrobbler/apps/music/migrations/0026_album_alt_names.py new file mode 100644 index 0000000..9db0661 --- /dev/null +++ b/vrobbler/apps/music/migrations/0026_album_alt_names.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.19 on 2025-04-07 00:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("music", "0025_artist_alt_names"), + ] + + operations = [ + migrations.AddField( + model_name="album", + name="alt_names", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/vrobbler/apps/music/models.py b/vrobbler/apps/music/models.py index ce7a0fd..6ceaf56 100644 --- a/vrobbler/apps/music/models.py +++ b/vrobbler/apps/music/models.py @@ -1,13 +1,11 @@ import logging -from tempfile import NamedTemporaryFile from typing import Dict, Optional -from urllib.request import urlopen from uuid import uuid4 import musicbrainzngs import requests from django.conf import settings -from django.core.files.base import ContentFile, File +from django.core.files.base import ContentFile from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -16,6 +14,7 @@ from imagekit.models import ImageSpecField from imagekit.processors import ResizeToFit from music.allmusic import get_allmusic_slug, scrape_data_from_allmusic from music.bandcamp import get_bandcamp_slug +from music.musicbrainz import lookup_album_dict_from_mb, lookup_track_from_mb from music.theaudiodb import lookup_album_from_tadb, lookup_artist_from_tadb from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin @@ -24,6 +23,16 @@ BNULL = {"blank": True, "null": True} class Artist(TimeStampedModel): + """Represents a music artist. + + # Lookup or create by title alone + >>> Artist.find_or_create(name="Bon Iver") + + # Lookup or create by MB id alone + >>> Artist.find_or_create(musicbrainz_id="0307edfc-437c-4b48-8700-80680e66a228") + + """ + uuid = models.UUIDField(default=uuid4, editable=False, **BNULL) name = models.CharField(max_length=255) biography = models.TextField(**BNULL) @@ -46,6 +55,7 @@ class Artist(TimeStampedModel): format="JPEG", options={"quality": 75}, ) + alt_names = models.TextField(**BNULL) class Meta: unique_together = [["name", "musicbrainz_id"]] @@ -62,8 +72,10 @@ class Artist(TimeStampedModel): return "" @property - def mb_link(self): - return f"https://musicbrainz.org/artist/{self.musicbrainz_id}" + def mb_link(self) -> str: + if self.musicbrainz_id: + return f"https://musicbrainz.org/artist/{self.musicbrainz_id}" + return "" @property def allmusic_link(self): @@ -104,7 +116,9 @@ class Artist(TimeStampedModel): if not self.allmusic_id or force: slug = get_allmusic_slug(self.name) if not slug: - logger.info(f"No allmsuic link for {self}") + logger.info( + "No allmusic link found", extra={"track_id": self.id} + ) return self.allmusic_id = slug self.save(update_fields=["allmusic_id"]) @@ -113,7 +127,9 @@ class Artist(TimeStampedModel): if not self.bandcamp_id or force: slug = get_bandcamp_slug(self.name) if not slug: - logger.info(f"No bandcamp link for {self}") + logger.info( + "No bandcamp link found", extra={"track_id": self.id} + ) return self.bandcamp_id = slug self.save(update_fields=["bandcamp_id"]) @@ -153,6 +169,61 @@ class Artist(TimeStampedModel): artist = self.name.lower() return f"https://bandcamp.com/search?q={artist}&item_type=b" + @classmethod + def find_or_create(cls, name: str, musicbrainz_id: str = "") -> "Artist": + from music.musicbrainz import lookup_artist_from_mb + from music.utils import clean_artist_name + + if not name: + raise Exception("Must have name to lookup artist") + + artist = None + name = clean_artist_name(name) + + # Check for name/mbid combo, just mbid and then just name + if musicbrainz_id: + artist = cls.objects.filter( + name=name, musicbrainz_id=musicbrainz_id + ).first() + if not artist: + artist = cls.objects.filter(musicbrainz_id=musicbrainz_id).first() + if not artist: + artist = cls.objects.filter( + models.Q(name=name) | models.Q(alt_names__icontains=name) + ).first() + + # Does not exist, look it up from Musicbrainz + if not artist: + alt_name = None + try: + artist_dict = lookup_artist_from_mb(name) + musicbrainz_id = musicbrainz_id or artist_dict.get("id", "") + if name != artist_dict.get("name", ""): + alt_name = name + name = artist_dict.get("name", "") + except ValueError: + pass + + if musicbrainz_id: + artist = cls.objects.filter( + musicbrainz_id=musicbrainz_id + ).first() + if artist and alt_name: + if not artist.alt_names: + artist.alt_names = alt_name + else: + artist.alt_names += f"\\{alt_name}" + artist.save(update_fields=["alt_names"]) + + if not artist: + artist = cls.objects.create( + name=name, musicbrainz_id=musicbrainz_id, alt_names=alt_name + ) + # TODO maybe this should be spun off into an async task? + artist.fix_metadata() + + return artist + class Album(TimeStampedModel): uuid = models.UUIDField(default=uuid4, editable=False, **BNULL) @@ -196,9 +267,10 @@ class Album(TimeStampedModel): wikipedia_slug = models.CharField(max_length=255, **BNULL) discogs_id = models.CharField(max_length=255, **BNULL) wikidata_id = models.CharField(max_length=255, **BNULL) + alt_names = models.TextField(**BNULL) - def __str__(self): - return self.name + def __str__(self) -> str: + return "{} by {}".format(self.name, self.album_artist) def get_absolute_url(self): return reverse("music:album_detail", kwargs={"slug": self.uuid}) @@ -402,6 +474,69 @@ class Album(TimeStampedModel): album = self.name.lower() return f"https://bandcamp.com/search?q={album} {artist}&item_type=a" + @classmethod + def find_or_create( + cls, name: str, artist_name: str, musicbrainz_id: str = "" + ) -> "Album": + if not name or not artist_name: + raise Exception( + "Must have at least name and artist name to lookup album" + ) + + album = None + if musicbrainz_id: + album = cls.objects.filter( + musicbrainz_id=musicbrainz_id, + name=name, + album_artist__name=artist_name, + ).first() + if not album and musicbrainz_id: + album = cls.objects.filter( + musicbrainz_id=musicbrainz_id, + ).first() + if not album: + album = cls.objects.filter( + models.Q(name=name) | models.Q(alt_names__icontains=name), + album_artist__name=artist_name, + ).first() + + if not album: + alt_name = None + try: + album_dict = lookup_album_dict_from_mb( + name, artist_name=artist_name + ) + musicbrainz_id = musicbrainz_id or album_dict.get("mb_id", "") + found_name = album_dict.get("title", "") + if found_name and name != found_name: + alt_name = name + name = found_name + except ValueError: + pass + + if musicbrainz_id: + album = cls.objects.filter( + musicbrainz_id=musicbrainz_id + ).first() + if album and alt_name: + if not album.alt_names: + album.alt_names = alt_name + else: + album.alt_names += f"\\{alt_name}" + album.save(update_fields=["alt_names"]) + if not album: + artist = Artist.find_or_create(name=artist_name) + album = cls.objects.create( + name=name, + album_artist=artist, + musicbrainz_id=musicbrainz_id, + alt_names=alt_name, + ) + # TODO maybe do this in a separate process? + album.fix_metadata() + + return album + class Track(ScrobblableMixin): COMPLETION_PERCENT = getattr(settings, "MUSIC_COMPLETION_PERCENT", 100) @@ -425,8 +560,8 @@ class Track(ScrobblableMixin): return reverse("music:track_detail", kwargs={"slug": self.uuid}) @property - def subtitle(self): - return self.artist + def subtitle(self) -> str: + return str(self.artist) @property def strings(self) -> ScrobblableConstants: @@ -451,31 +586,85 @@ class Track(ScrobblableMixin): @classmethod def find_or_create( - cls, artist_dict: Dict, album_dict: Dict, track_dict: Dict - ) -> Optional["Track"]: - """Given a data dict from Jellyfin, does the heavy lifting of looking up - the video and, if need, TV Series, creating both if they don't yet - exist. + cls, + title: str = "", + musicbrainz_id: str = "", + album_name: str = "", + artist_name: str = "", + enrich: bool = True, + run_time_seconds: Optional[int] = None, + ) -> "Track": + # TODO we can use Q to build queries here based on whether we have mbid and album name + track = None + # Full look up with MB ID + if album_name: + track = cls.objects.filter( + musicbrainz_id=musicbrainz_id, + title=title, + artist__name=artist_name, + album__name=album_name, + ).first() + # Full look up without album + if not track: + track = cls.objects.filter( + musicbrainz_id=musicbrainz_id, + title=title, + artist__name=artist_name, + ).first() - """ - if not artist_dict.get("name") or not artist_dict.get( - "musicbrainz_id" - ): - logger.warning( - f"No artist or artist musicbrainz ID found in message from source, not scrobbling" - ) - return + # Full look up without MB ID + if not track: + track = cls.objects.filter( + title=title, + artist__name=artist_name, + album__name=album_name, + ).first() + # Base look up without MB ID or album + if not track: + track = cls.objects.filter( + title=title, + artist__name=artist_name, + ).first() - artist, artist_created = Artist.objects.get_or_create(**artist_dict) - album, album_created = Album.objects.get_or_create(**album_dict) + if not track and enrich: + track_dict = lookup_track_from_mb(title, artist_name, album_name) + musicbrainz_id = musicbrainz_id or track_dict.get("id", "") + # TODO This only works some of the time + # try: + # album_name = album_name or track_dict.get("release-list")[ + # 0 + # ].get("title", "") + # except IndexError: + # pass + if not run_time_seconds: + run_time_seconds = int( + int(track_dict.get("length", 900000)) / 1000 + ) + if title != track_dict.get("name", "") and track_dict.get( + "name", False + ): - album.fix_metadata() - if not album.cover_image: - album.fetch_artwork() + title = track_dict.get("name", "") - track_dict["album_id"] = getattr(album, "id", None) - track_dict["artist_id"] = artist.id - - track, created = cls.objects.get_or_create(**track_dict) + if musicbrainz_id: + track = cls.objects.filter( + musicbrainz_id=musicbrainz_id + ).first() + if not track: + artist = Artist.find_or_create(name=artist_name) + album = None + if album_name: + album = Album.find_or_create( + name=album_name, artist_name=artist_name + ) + track = cls.objects.create( + title=title, + album=album, + musicbrainz_id=musicbrainz_id, + artist=artist, + run_time_seconds=run_time_seconds, + ) + # TODO maybe do this in a separate process? + track.fix_metadata() return track diff --git a/vrobbler/apps/music/utils.py b/vrobbler/apps/music/utils.py index 4e6f9b9..c050c02 100644 --- a/vrobbler/apps/music/utils.py +++ b/vrobbler/apps/music/utils.py @@ -16,9 +16,8 @@ logger = logging.getLogger(__name__) from music.models import Album, Artist, Track -def get_or_create_artist(name: str, mbid: str = None) -> Artist: - artist = None - +def clean_artist_name(name: str) -> str: + """Remove featured names from artist string.""" if "feat." in name.lower(): name = re.split("feat.", name, flags=re.IGNORECASE)[0].strip() if "featuring" in name.lower(): @@ -26,18 +25,44 @@ def get_or_create_artist(name: str, mbid: str = None) -> Artist: if "&" in name.lower(): name = re.split("&", name, flags=re.IGNORECASE)[0].strip() - artist_dict = lookup_artist_from_mb(name) - mbid = mbid or artist_dict.get("id", None) + return name - if mbid: + +# TODO These are depreacted, remove them eventually +def get_or_create_artist(name: str, mbid: str = "") -> Artist: + """Get an Artist object from the database. + + Check if an artist with this name or Musicbrainz ID already exists. + Otherwise, go lookup artist data from Musicbrainz and create one. + + """ + artist = None + name = clean_artist_name(name) + + # Check for name/mbid combo, just mbid and then just name + artist = Artist.objects.filter(name=name, mbid=mbid).first() + if not artist: artist = Artist.objects.filter(musicbrainz_id=mbid).first() + if not artist: + artist = Artist.objects.filter(name=name).first() + + # Does not exist, look it up from Musicbrainz + if not artist: + artist_dict = lookup_artist_from_mb(name) + mbid = mbid or artist_dict.get("id", "") + + if mbid: + artist = Artist.objects.filter(musicbrainz_id=mbid).first() + if not artist: artist = Artist.objects.create(name=name, musicbrainz_id=mbid) + # TODO maybe this should be spun off into an async task? artist.fix_metadata() return artist +# TODO These are depreacted, remove them eventually def get_or_create_album( name: str, artist: Artist, mbid: str = None ) -> Optional[Album]: @@ -90,6 +115,7 @@ def get_or_create_album( return album +# TODO These are depreacted, remove them eventually def get_or_create_track(post_data: dict, post_keys: dict) -> Track: try: track_run_time_seconds = int( @@ -107,16 +133,12 @@ def get_or_create_track(post_data: dict, post_keys: dict) -> Track: track_title = post_data.get(post_keys.get("TRACK_TITLE"), "") track_mb_id = post_data.get(post_keys.get("TRACK_MB_ID"), "") - artist = get_or_create_artist( - artist_name, - mbid=artist_mb_id, - ) + artist = Artist.find_or_create(artist_name, artist_mb_id) album = None - if album_mb_id: - album = get_or_create_album( - album_title, - artist=artist, - mbid=album_mb_id, + # We may get no album ID or title, in which case, skip + if album_mb_id or album_title: + album = Album.find_or_create( + album_title, str(artist.name), album_mb_id ) track = None @@ -154,7 +176,7 @@ def get_or_create_track(post_data: dict, post_keys: dict) -> Track: return track -def get_or_create_various_artists(): +def get_or_create_various_artists() -> Artist: artist = Artist.objects.filter(name="Various Artists").first() if not artist: artist = Artist.objects.create(**VARIOUS_ARTIST_DICT) diff --git a/vrobbler/apps/scrobbles/mixins.py b/vrobbler/apps/scrobbles/mixins.py index c572d3c..812e379 100644 --- a/vrobbler/apps/scrobbles/mixins.py +++ b/vrobbler/apps/scrobbles/mixins.py @@ -124,12 +124,12 @@ class ScrobblableMixin(TimeStampedModel): logger.warning("fix_metadata() not implemented yet") @classmethod - def find_or_create(cls) -> None: + def find_or_create(cls): logger.warning("find_or_create() not implemented yet") - def __str__(self): + def __str__(self) -> str: if self.title: - return self.title + return str(self.title) return str(self.uuid) diff --git a/vrobbler/apps/scrobbles/scrobblers.py b/vrobbler/apps/scrobbles/scrobblers.py index 17a3701..dc2056c 100644 --- a/vrobbler/apps/scrobbles/scrobblers.py +++ b/vrobbler/apps/scrobbles/scrobblers.py @@ -1,8 +1,7 @@ -from datetime import datetime import logging import re +from datetime import datetime from typing import Optional -from urllib.parse import parse_qs, urlparse import pendulum import pytz @@ -56,7 +55,11 @@ def mopidy_scrobble_media(post_data: dict, user_id: int) -> Scrobble: if media_type == Scrobble.MediaType.PODCAST_EPISODE: media_obj = get_or_create_podcast(post_data) else: - media_obj = get_or_create_track(post_data, MOPIDY_POST_KEYS) + media_obj = Track.find_or_create( + title=post_data.get("title", ""), + album_name=post_data.get("album", ""), + run_time_seconds=post_data.get("run_time", 900000), + ) log = {} try: @@ -109,8 +112,11 @@ def jellyfin_scrobble_media( post_data.get("Provider_imdb", "").replace("tt", "") ) else: - media_obj = get_or_create_track( - post_data, post_keys=JELLYFIN_POST_KEYS + media_obj = Track.find_or_create( + title=post_data.get("Name", ""), + album_name=post_data.get("Album", ""), + run_time_seconds=post_data.get("RunTime", 900000), + musicbrainz_id=post_data.get("Provider_musicbrainztrack", ""), ) # A hack because we don't worry about updating music ... we either finish it or we don't playback_position_seconds = 0 diff --git a/vrobbler/apps/scrobbles/tsv.py b/vrobbler/apps/scrobbles/tsv.py index 9167512..d91f18f 100644 --- a/vrobbler/apps/scrobbles/tsv.py +++ b/vrobbler/apps/scrobbles/tsv.py @@ -1,18 +1,12 @@ import codecs import csv import logging -from datetime import datetime import pytz import requests -from music.utils import ( - get_or_create_album, - get_or_create_artist, - get_or_create_track, -) +from music.models import Track from scrobbles.constants import AsTsvColumn from scrobbles.models import Scrobble -from music.constants import MOPIDY_POST_KEYS from scrobbles.utils import timestamp_user_tz_to_utc @@ -50,27 +44,12 @@ def process_audioscrobbler_tsv_file(file_path, user_id, user_tz=None): ) continue - track = get_or_create_track( - { - "title": row[AsTsvColumn["TRACK_NAME"].value], - "mbid": row[AsTsvColumn["MB_ID"].value], - "artist_name": row[AsTsvColumn["ARTIST_NAME"].value], - "album_name": row[AsTsvColumn["ALBUM_NAME"].value], - "run_time_seconds": int( - row[AsTsvColumn["RUN_TIME_SECONDS"].value] - ), - }, - { - "TRACK_MB_ID": "mbid", - "TRACK_TITLE": "track_title", - "ALBUM_NAME": "album_name", - "ARTIST_NAME": "artist_name", - "RUN_TIME": "run_time_seconds", - }, + track = Track.find_or_create( + title=row[AsTsvColumn["TRACK_NAME"].value], + musicbrainz_id=row[AsTsvColumn["MB_ID"].value], + artist_name=row[AsTsvColumn["ARTIST_NAME"].value] + album_name=row[AsTsvColumn["ALBUM_NAME"].value] ) - if not track: - logger.info(f"Skipping track {track} because not found") - continue # TODO Set all this up as constants if row[AsTsvColumn["COMPLETE"].value] == "S": From 499c3d685997c569967d67288296dfb066796b21 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Sun, 6 Apr 2025 22:40:33 -0400 Subject: [PATCH 02/14] [music] Fix bug in scobblers for tracks --- vrobbler/apps/scrobbles/scrobblers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vrobbler/apps/scrobbles/scrobblers.py b/vrobbler/apps/scrobbles/scrobblers.py index dc2056c..4e60666 100644 --- a/vrobbler/apps/scrobbles/scrobblers.py +++ b/vrobbler/apps/scrobbles/scrobblers.py @@ -56,7 +56,8 @@ def mopidy_scrobble_media(post_data: dict, user_id: int) -> Scrobble: media_obj = get_or_create_podcast(post_data) else: media_obj = Track.find_or_create( - title=post_data.get("title", ""), + title=post_data.get("name", ""), + artist_name=post_data.get("artist", ""), album_name=post_data.get("album", ""), run_time_seconds=post_data.get("run_time", 900000), ) @@ -114,6 +115,7 @@ def jellyfin_scrobble_media( else: media_obj = Track.find_or_create( title=post_data.get("Name", ""), + artist_name=post_data.get("Artist", ""), album_name=post_data.get("Album", ""), run_time_seconds=post_data.get("RunTime", 900000), musicbrainz_id=post_data.get("Provider_musicbrainztrack", ""), From 27f50baf5dbc28eaccace63763262bcf610860e1 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Sun, 6 Apr 2025 23:39:32 -0400 Subject: [PATCH 03/14] [music] Fixes missing jellyfin ts converter --- vrobbler/apps/scrobbles/scrobblers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vrobbler/apps/scrobbles/scrobblers.py b/vrobbler/apps/scrobbles/scrobblers.py index 4e60666..07c0eb7 100644 --- a/vrobbler/apps/scrobbles/scrobblers.py +++ b/vrobbler/apps/scrobbles/scrobblers.py @@ -22,6 +22,7 @@ from scrobbles.constants import ( SCROBBLE_CONTENT_URLS, ) from scrobbles.models import Scrobble +from scrobbles.utils import convert_to_seconds from sports.models import SportEvent from sports.thesportsdb import lookup_event_from_thesportsdb from tasks.models import Task @@ -117,7 +118,9 @@ def jellyfin_scrobble_media( title=post_data.get("Name", ""), artist_name=post_data.get("Artist", ""), album_name=post_data.get("Album", ""), - run_time_seconds=post_data.get("RunTime", 900000), + run_time_seconds=convert_to_seconds( + post_data.get("RunTime", 900000) + ), musicbrainz_id=post_data.get("Provider_musicbrainztrack", ""), ) # A hack because we don't worry about updating music ... we either finish it or we don't From 0bb874f1db35b4f3da5797b8805ab722d254c8ba Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 7 Apr 2025 00:12:29 -0400 Subject: [PATCH 04/14] [scrobblers] Fixes test with wiggly second --- tests/scrobbles_tests/test_views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/scrobbles_tests/test_views.py b/tests/scrobbles_tests/test_views.py index c65f29b..5feb5a9 100644 --- a/tests/scrobbles_tests/test_views.py +++ b/tests/scrobbles_tests/test_views.py @@ -111,7 +111,9 @@ def test_scrobble_mopidy_same_track_different_album( assert scrobble.media_obj.title == "Same in the End" -@pytest.mark.skip("Need to add a mock podcast request data, tho Google Podcasts is gone :thinking:") +@pytest.mark.skip( + "Need to add a mock podcast request data, tho Google Podcasts is gone :thinking:" +) @pytest.mark.django_db def test_scrobble_mopidy_podcast( client, mopidy_podcast_request_data, valid_auth_token @@ -193,7 +195,7 @@ def test_scrobble_jellyfin_track( assert scrobble.media_obj.__class__ == Track assert scrobble.media_obj.title == "Emotion" - with time_machine.travel(datetime(2024, 1, 14, 12, 1, 1)): + with time_machine.travel(datetime(2024, 1, 14, 12, 1, 4)): jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime( "%Y-%m-%d %H:%M:%S" ) From 28cf57c6dd0124f3d68a53535e13796735e750ca Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 7 Apr 2025 09:56:10 -0400 Subject: [PATCH 05/14] [tests] Try fixing CI breaking with small seconds --- tests/scrobbles_tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scrobbles_tests/test_views.py b/tests/scrobbles_tests/test_views.py index 5feb5a9..df35218 100644 --- a/tests/scrobbles_tests/test_views.py +++ b/tests/scrobbles_tests/test_views.py @@ -195,7 +195,7 @@ def test_scrobble_jellyfin_track( assert scrobble.media_obj.__class__ == Track assert scrobble.media_obj.title == "Emotion" - with time_machine.travel(datetime(2024, 1, 14, 12, 1, 4)): + with time_machine.travel(datetime(2024, 1, 14, 12, 1, 10)): jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime( "%Y-%m-%d %H:%M:%S" ) From 6c461ed55fa36231a41570141dbba11fd8eeb7be Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 7 Apr 2025 12:30:54 -0400 Subject: [PATCH 06/14] [tests] Simplify view tests to not mess with time --- tests/scrobbles_tests/conftest.py | 12 ++- tests/scrobbles_tests/test_views.py | 142 ++++++++++++++++++++++------ vrobbler/apps/scrobbles/models.py | 7 +- 3 files changed, 125 insertions(+), 36 deletions(-) diff --git a/tests/scrobbles_tests/conftest.py b/tests/scrobbles_tests/conftest.py index 9725ec4..488393d 100644 --- a/tests/scrobbles_tests/conftest.py +++ b/tests/scrobbles_tests/conftest.py @@ -4,8 +4,9 @@ import pytest from django.contrib.auth import get_user_model from rest_framework.authtoken.models import Token -from scrobbles.models import Scrobble from boardgames.models import BoardGame +from music.models import Track, Artist +from scrobbles.models import Scrobble User = get_user_model() @@ -27,6 +28,15 @@ def boardgame_scrobble(): ) +@pytest.fixture +def test_track(): + Track.objects.create( + title="Emotion", + artist=Artist.objects.create(name="Carly Rae Jepsen"), + run_time_seconds=60, + ) + + class MopidyRequest: name = "Same in the End" artist = "Sublime" diff --git a/tests/scrobbles_tests/test_views.py b/tests/scrobbles_tests/test_views.py index df35218..86290e6 100644 --- a/tests/scrobbles_tests/test_views.py +++ b/tests/scrobbles_tests/test_views.py @@ -76,9 +76,26 @@ def test_scrobble_mopidy_track( assert scrobble.media_obj.title == "Same in the End" -@pytest.mark.skip(reason="Allmusic API is unstable") +# @pytest.mark.skip(reason="Allmusic API is unstable") @pytest.mark.django_db +@patch("music.utils.lookup_artist_from_mb", return_value={}) +@patch( + "music.utils.lookup_album_dict_from_mb", + return_value={"year": "1999", "mb_group_id": 1}, +) +@patch("music.utils.lookup_track_from_mb", return_value={}) +@patch("music.models.lookup_artist_from_tadb", return_value={}) +@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"}) +@patch("music.models.Album.fetch_artwork", return_value=None) +@patch("music.models.Album.scrape_allmusic", return_value=None) def test_scrobble_mopidy_same_track_different_album( + mock_lookup_artist, + mock_lookup_album, + mock_lookup_track, + mock_lookup_artist_tadb, + mock_lookup_album_tadb, + mock_fetch_artwork, + mock_scrape_allmusic, client, mopidy_track, mopidy_track_diff_album_request_data, @@ -177,38 +194,101 @@ def test_scrobble_jellyfin_track( assert scrobble.media_obj.__class__ == Track assert scrobble.media_obj.title == "Emotion" - with time_machine.travel(datetime(2024, 1, 14, 12, 0, 58)): - jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime( - "%Y-%m-%d %H:%M:%S" - ) - response = client.post( - url, - jellyfin_track.request_json, - content_type="application/json", - headers=headers, - ) - assert response.status_code == 200 - assert response.data == {"scrobble_id": 1} +@pytest.mark.django_db +@patch("music.utils.lookup_artist_from_mb", return_value={}) +@patch( + "music.utils.lookup_album_dict_from_mb", + return_value={"year": "1999", "mb_group_id": 1}, +) +@patch("music.utils.lookup_track_from_mb", return_value={}) +@patch("music.models.lookup_artist_from_tadb", return_value={}) +@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"}) +@patch("music.models.Album.fetch_artwork", return_value=None) +@patch("music.models.Album.scrape_allmusic", return_value=None) +def test_scrobble_jellyfin_track_update( + mock_lookup_artist, + mock_lookup_album, + mock_lookup_track, + mock_lookup_artist_tadb, + mock_lookup_album_tadb, + mock_fetch_artwork, + mock_scrape_allmusic, + test_track, + client, + jellyfin_track, + valid_auth_token, +): + Scrobble.objects.create( + timestamp=timezone.now() - timedelta(minutes=0.5), + track=Track.objects.first(), + user_id=1, + ) + url = reverse("scrobbles:jellyfin-webhook") + headers = {"Authorization": f"Token {valid_auth_token}"} - scrobble = Scrobble.objects.get(id=1) - assert scrobble.media_obj.__class__ == Track - assert scrobble.media_obj.title == "Emotion" + jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime( + "%Y-%m-%d %H:%M:%S" + ) + response = client.post( + url, + jellyfin_track.request_json, + content_type="application/json", + headers=headers, + ) - with time_machine.travel(datetime(2024, 1, 14, 12, 1, 10)): - jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime( - "%Y-%m-%d %H:%M:%S" - ) - response = client.post( - url, - jellyfin_track.request_json, - content_type="application/json", - headers=headers, - ) + assert response.status_code == 200 + assert response.data == {"scrobble_id": 1} - assert response.status_code == 200 - assert response.data == {"scrobble_id": 2} + scrobble = Scrobble.objects.get(id=1) + assert scrobble.media_obj.__class__ == Track + assert scrobble.media_obj.title == "Emotion" - scrobble = Scrobble.objects.get(id=1) - assert scrobble.media_obj.__class__ == Track - assert scrobble.media_obj.title == "Emotion" + +@pytest.mark.django_db +@patch("music.utils.lookup_artist_from_mb", return_value={}) +@patch( + "music.utils.lookup_album_dict_from_mb", + return_value={"year": "1999", "mb_group_id": 1}, +) +@patch("music.utils.lookup_track_from_mb", return_value={}) +@patch("music.models.lookup_artist_from_tadb", return_value={}) +@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"}) +@patch("music.models.Album.fetch_artwork", return_value=None) +@patch("music.models.Album.scrape_allmusic", return_value=None) +def test_scrobble_jellyfin_track_create_new( + mock_lookup_artist, + mock_lookup_album, + mock_lookup_track, + mock_lookup_artist_tadb, + mock_lookup_album_tadb, + mock_fetch_artwork, + mock_scrape_allmusic, + test_track, + client, + jellyfin_track, + valid_auth_token, +): + url = reverse("scrobbles:jellyfin-webhook") + headers = {"Authorization": f"Token {valid_auth_token}"} + Scrobble.objects.create( + timestamp=timezone.now() - timedelta(minutes=1), + track=Track.objects.first(), + user_id=1, + ) + jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime( + "%Y-%m-%d %H:%M:%S" + ) + response = client.post( + url, + jellyfin_track.request_json, + content_type="application/json", + headers=headers, + ) + + assert response.status_code == 200 + assert response.data == {"scrobble_id": 2} + + scrobble = Scrobble.objects.get(id=1) + assert scrobble.media_obj.__class__ == Track + assert scrobble.media_obj.title == "Emotion" diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index a99f852..87f14b6 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/vrobbler/apps/scrobbles/models.py @@ -1034,7 +1034,6 @@ class Scrobble(TimeStampedModel): "scrobble_data": scrobble_data, }, ) - scrobble_data["playback_status"] = scrobble_data.pop("status", None) # If it's marked as stopped, send it through our update mechanism, which will complete it if scrobble and ( @@ -1140,9 +1139,9 @@ class Scrobble(TimeStampedModel): if existing_locations := location.in_proximity(named=True): existing_location = existing_locations.first() ts = int(pendulum.now().timestamp()) - scrobble.log[ts] = ( - f"Location {location.id} too close to this scrobble" - ) + scrobble.log[ + ts + ] = f"Location {location.id} too close to this scrobble" scrobble.save(update_fields=["log"]) logger.info( f"[scrobbling] finished - found existing named location", From bcc3f46806ce7a7c7d96ac99676d24ff476e6166 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 7 Apr 2025 12:39:57 -0400 Subject: [PATCH 07/14] [tests] We dont differentiate on albums anymore --- tests/scrobbles_tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scrobbles_tests/test_views.py b/tests/scrobbles_tests/test_views.py index 86290e6..6294ab2 100644 --- a/tests/scrobbles_tests/test_views.py +++ b/tests/scrobbles_tests/test_views.py @@ -124,7 +124,7 @@ def test_scrobble_mopidy_same_track_different_album( assert response.data == {"scrobble_id": 2} scrobble = Scrobble.objects.last() assert scrobble.media_obj.__class__ == Track - assert scrobble.media_obj.album.name == "Gold" + assert scrobble.media_obj.album.name == "Sublime" assert scrobble.media_obj.title == "Same in the End" From 4767cc7e52629d94b0e13c5d56163954492a41d5 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 7 Apr 2025 13:30:29 -0400 Subject: [PATCH 08/14] [podcasts] Fixes enrichment of podcasts with podcastindex --- ...podcasts_url_podcast_dead_date_and_more.py | 40 +++++ .../podcasts/migrations/0016_podcast_genre.py | 28 ++++ .../0017_podcast_podcastindex_id.py | 18 +++ vrobbler/apps/podcasts/models.py | 140 +++++++++++------- .../apps/podcasts/sources/podcastindex.py | 75 ++++++++++ vrobbler/apps/scrobbles/scrobblers.py | 17 ++- vrobbler/settings.py | 2 + 7 files changed, 265 insertions(+), 55 deletions(-) create mode 100644 vrobbler/apps/podcasts/migrations/0015_remove_podcast_google_podcasts_url_podcast_dead_date_and_more.py create mode 100644 vrobbler/apps/podcasts/migrations/0016_podcast_genre.py create mode 100644 vrobbler/apps/podcasts/migrations/0017_podcast_podcastindex_id.py create mode 100644 vrobbler/apps/podcasts/sources/podcastindex.py diff --git a/vrobbler/apps/podcasts/migrations/0015_remove_podcast_google_podcasts_url_podcast_dead_date_and_more.py b/vrobbler/apps/podcasts/migrations/0015_remove_podcast_google_podcasts_url_podcast_dead_date_and_more.py new file mode 100644 index 0000000..e6bc06f --- /dev/null +++ b/vrobbler/apps/podcasts/migrations/0015_remove_podcast_google_podcasts_url_podcast_dead_date_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 4.2.19 on 2025-04-07 17:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("podcasts", "0014_alter_podcastepisode_run_time_seconds"), + ] + + operations = [ + migrations.RemoveField( + model_name="podcast", + name="google_podcasts_url", + ), + migrations.AddField( + model_name="podcast", + name="dead_date", + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name="podcast", + name="itunes_id", + field=models.TextField(blank=True, max_length=15, null=True), + ), + migrations.AddField( + model_name="podcast", + name="null", + field=models.CharField( + default="", max_length=150, verbose_name="blank" + ), + preserve_default=False, + ), + migrations.AddField( + model_name="podcast", + name="site_link", + field=models.URLField(blank=True, null=True), + ), + ] diff --git a/vrobbler/apps/podcasts/migrations/0016_podcast_genre.py b/vrobbler/apps/podcasts/migrations/0016_podcast_genre.py new file mode 100644 index 0000000..65e4926 --- /dev/null +++ b/vrobbler/apps/podcasts/migrations/0016_podcast_genre.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.19 on 2025-04-07 17:18 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ("scrobbles", "0068_scrobble_paper_alter_scrobble_media_type"), + ( + "podcasts", + "0015_remove_podcast_google_podcasts_url_podcast_dead_date_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="podcast", + name="genre", + field=taggit.managers.TaggableManager( + help_text="A comma-separated list of tags.", + through="scrobbles.ObjectWithGenres", + to="scrobbles.Genre", + verbose_name="Tags", + ), + ), + ] diff --git a/vrobbler/apps/podcasts/migrations/0017_podcast_podcastindex_id.py b/vrobbler/apps/podcasts/migrations/0017_podcast_podcastindex_id.py new file mode 100644 index 0000000..322c3f2 --- /dev/null +++ b/vrobbler/apps/podcasts/migrations/0017_podcast_podcastindex_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.19 on 2025-04-07 17:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("podcasts", "0016_podcast_genre"), + ] + + operations = [ + migrations.AddField( + model_name="podcast", + name="podcastindex_id", + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/vrobbler/apps/podcasts/models.py b/vrobbler/apps/podcasts/models.py index 9e56f15..b046b88 100644 --- a/vrobbler/apps/podcasts/models.py +++ b/vrobbler/apps/podcasts/models.py @@ -1,5 +1,4 @@ import logging -from typing import Dict, Optional from uuid import uuid4 import requests @@ -10,8 +9,16 @@ from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django_extensions.db.models import TimeStampedModel -from podcasts.scrapers import scrape_data_from_google_podcasts -from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin +from scrobbles.mixins import ( + ObjectWithGenres, + ScrobblableConstants, + ScrobblableMixin, +) +from taggit.managers import TaggableManager + +from podcasts.sources.podcastindex import ( + lookup_podcast_from_podcastindex, +) logger = logging.getLogger(__name__) BNULL = {"blank": True, "null": True} @@ -24,6 +31,13 @@ class Producer(TimeStampedModel): def __str__(self): return f"{self.name}" + @classmethod + def find_or_create(cls, name): + producer = cls.objects.filter(name__iexact=name).first() + if not producer: + producer = cls.objects.create(name=name) + return producer + class Podcast(TimeStampedModel): name = models.CharField(max_length=255) @@ -31,11 +45,17 @@ class Podcast(TimeStampedModel): producer = models.ForeignKey( Producer, on_delete=models.DO_NOTHING, **BNULL ) + podcastindex_id = models.CharField(max_length=100, **BNULL) + owner = models.CharField(max_length=150, *BNULL) description = models.TextField(**BNULL) active = models.BooleanField(default=True) feed_url = models.URLField(**BNULL) - google_podcasts_url = models.URLField(**BNULL) + site_link = models.URLField(**BNULL) + description = models.TextField(**BNULL) cover_image = models.ImageField(upload_to="podcasts/covers/", **BNULL) + itunes_id = models.TextField(max_length=15, **BNULL) + dead_date = models.DateField(**BNULL) + genre = TaggableManager(through=ObjectWithGenres) def __str__(self): return f"{self.name}" @@ -49,32 +69,43 @@ class Podcast(TimeStampedModel): user=user, podcast_episode__podcast=self ).order_by("-timestamp") - def scrape_google_podcasts(self, force=False): - podcast_dict = {} - if not self.cover_image or force: - podcast_dict = scrape_data_from_google_podcasts(self.name) - if podcast_dict: - if not self.producer: - self.producer, created = Producer.objects.get_or_create( - name=podcast_dict["producer"] - ) - self.description = podcast_dict.get("description") - self.google_podcasts_url = podcast_dict.get("google_url") - self.save( - update_fields=[ - "description", - "producer", - "google_podcasts_url", - ] - ) + @property + def itunes_link(self) -> str: + if not self.itunes_id: + return "" + return f"https://podcasts.apple.com/us/podcast/id{self.itunes_id}" + + def fix_metadata(self, force=False): + if self.podcastindex_id and not force: + logger.warning( + "Podcast already has PodcastIndex ID, use force=True to overwrite" + ) + return + + podcast_dict = lookup_podcast_from_podcastindex(self.name) + + if not podcast_dict: + logger.info( + "No podcast data found from PodcastIndex. Are credentials setup?" + ) + return + + genres = podcast_dict.pop("genres") + if genres: + self.genre.add(*genres) + + cover_url = podcast_dict.pop("image_url") - cover_url = podcast_dict.get("image_url") if (not self.cover_image or force) and cover_url: r = requests.get(cover_url) if r.status_code == 200: fname = f"{self.name}_{self.uuid}.jpg" self.cover_image.save(fname, ContentFile(r.content), save=True) + for attr, value in podcast_dict.items(): + setattr(self, attr, value) + self.save() + class PodcastEpisode(ScrobblableMixin): COMPLETION_PERCENT = getattr(settings, "PODCAST_COMPLETION_PERCENT", 90) @@ -108,42 +139,45 @@ class PodcastEpisode(ScrobblableMixin): @classmethod def find_or_create( - cls, podcast_dict: Dict, producer_dict: Dict, episode_dict: Dict - ) -> Optional["Episode"]: + cls, + title: str, + podcast_name: str, + pub_date: str, + number: int = 0, + mopidy_uri: str = "", + producer_name: str = "", + run_time_seconds: int = 1800, + enrich: bool = True, + ) -> "PodcastEpisode": """Given a data dict from Mopidy, finds or creates a podcast and producer before saving the epsiode so it can be scrobbled. """ - if not podcast_dict.get("name"): - logger.warning(f"No name from source for podcast, not scrobbling") - return - producer = None - if producer_dict.get("name"): - producer, producer_created = Producer.objects.get_or_create( - **producer_dict + if producer_name: + producer = Producer.find_or_create(producer_name) + + podcast = Podcast.objects.filter( + name__iexact=podcast_name, + ).first() + if not podcast: + podcast = Podcast.objects.create( + name=podcast_name, producer=producer ) - if producer_created: - logger.debug(f"Created new producer {producer}") - else: - logger.debug(f"Found producer {producer}") + if enrich: + podcast.fix_metadata() - if producer: - podcast_dict["producer_id"] = producer.id - podcast, podcast_created = Podcast.objects.get_or_create( - **podcast_dict - ) - if podcast_created: - logger.debug(f"Created new podcast {podcast}") - else: - logger.debug(f"Found podcast {podcast}") - - episode_dict["podcast_id"] = podcast.id - - episode, created = cls.objects.get_or_create(**episode_dict) - if created: - logger.debug(f"Created new episode: {episode}") - else: - logger.debug(f"Found episode {episode}") + episode = cls.objects.filter( + title__iexact=title, podcast=podcast + ).first() + if not episode: + episode = cls.objects.create( + title=title, + podcast=podcast, + run_time_seconds=run_time_seconds, + number=number, + pub_date=pub_date, + mopidy_uri=mopidy_uri, + ) return episode diff --git a/vrobbler/apps/podcasts/sources/podcastindex.py b/vrobbler/apps/podcasts/sources/podcastindex.py new file mode 100644 index 0000000..88f3563 --- /dev/null +++ b/vrobbler/apps/podcasts/sources/podcastindex.py @@ -0,0 +1,75 @@ +import hashlib +import time + +import pytz +import requests +from django.conf import settings +from django.utils import timezone +from scrobbles.utils import timestamp_user_tz_to_utc + +PODCASTINDEX_API_KEY = getattr(settings, "PODCASTINDEX_API_KEY") +PODCASTINDEX_API_SECRET = getattr(settings, "PODCASTINDEX_API_SECRET") + + +def get_auth_headers(): + now = int(time.time()) + hash_data = hashlib.sha1( + (PODCASTINDEX_API_KEY + PODCASTINDEX_API_SECRET + str(now)).encode( + "utf-8" + ) + ).hexdigest() + + return { + "User-Agent": "MyPodcastApp/1.0", + "X-Auth-Date": str(now), + "X-Auth-Key": PODCASTINDEX_API_KEY, + "Authorization": hash_data, + "Content-Type": "application/json", + } + + +def lookup_podcast_from_podcastindex( + podcast_name: str, dump_raw_response: bool = False +) -> dict: + url = "https://api.podcastindex.org/api/1.0/search/byterm" + headers = get_auth_headers() + params = {"q": podcast_name} + + response = requests.get(url, headers=headers, params=params) + + if response.status_code == 200: + data = response.json() + if dump_raw_response: + return data.get("feeds") + if data.get("feeds"): + try: + top_feed_dict = data["feeds"][0] + + newest_episode_date = timestamp_user_tz_to_utc( + top_feed_dict.get("newestItemPubdate"), pytz.UTC + ) + days_since_last_episode = () + dead_date = None + if (timezone.now() - newest_episode_date).days > 180: + dead_date = newest_episode_date + + return { + "podcastindex_id": top_feed_dict.get("id"), + "title": top_feed_dict.get("title"), + "site_link": top_feed_dict.get("link"), + "description": top_feed_dict.get("description"), + "owner": top_feed_dict.get("ownerName"), + "image_url": top_feed_dict.get("artwork"), + "feed_url": top_feed_dict.get("url"), + "itunes_id": top_feed_dict.get("itunesId"), + "genres": list(top_feed_dict.get("categories").values()), + "dead_date": dead_date, + } + except IndexError: + return {} + else: + print("No podcasts found.") + return {} + else: + print("Failed to fetch data:", response.status_code, response.text) + return {} diff --git a/vrobbler/apps/scrobbles/scrobblers.py b/vrobbler/apps/scrobbles/scrobblers.py index 07c0eb7..2ba6dec 100644 --- a/vrobbler/apps/scrobbles/scrobblers.py +++ b/vrobbler/apps/scrobbles/scrobblers.py @@ -15,7 +15,8 @@ from locations.models import GeoLocation from music.constants import JELLYFIN_POST_KEYS, MOPIDY_POST_KEYS from music.models import Track from music.utils import get_or_create_track -from podcasts.utils import get_or_create_podcast +from podcasts.models import PodcastEpisode +from podcasts.utils import parse_mopidy_uri from scrobbles.constants import ( JELLYFIN_AUDIO_ITEM_TYPES, MANUAL_SCROBBLE_FNS, @@ -54,7 +55,19 @@ def mopidy_scrobble_media(post_data: dict, user_id: int) -> Scrobble: ) if media_type == Scrobble.MediaType.PODCAST_EPISODE: - media_obj = get_or_create_podcast(post_data) + parsed_data = parse_mopidy_uri(post_data.get("mopidy_uri", "")) + podcast_name = post_data.get( + "album", parsed_data.get("podcast_name", "") + ) + + media_obj = PodcastEpisode.find_or_create( + title=parsed_data.get("episode_filename", ""), + podcast_name=podcast_name, + producer_name=post_data.get("artist", ""), + number=parsed_data.get("episode_num", ""), + pub_date=parsed_data.get("pub_date", ""), + mopidy_uri=post_data.get("mopidy_uri", ""), + ) else: media_obj = Track.find_or_create( title=post_data.get("name", ""), diff --git a/vrobbler/settings.py b/vrobbler/settings.py index 17aa6ee..74996ae 100644 --- a/vrobbler/settings.py +++ b/vrobbler/settings.py @@ -60,6 +60,8 @@ DUMP_REQUEST_DATA = ( THESPORTSDB_API_KEY = os.getenv("VROBBLER_THESPORTSDB_API_KEY", "2") THEAUDIODB_API_KEY = os.getenv("VROBBLER_THEAUDIODB_API_KEY", "2") +PODCASTINDEX_API_KEY = os.getenv("VROBBLER_PODCASTINDEX_API_KEY", "") +PODCASTINDEX_API_SECRET = os.getenv("VROBBLER_PODCASTINDEX_API_SECRET", "") TMDB_API_KEY = os.getenv("VROBBLER_TMDB_API_KEY", "") LASTFM_API_KEY = os.getenv("VROBBLER_LASTFM_API_KEY") LASTFM_SECRET_KEY = os.getenv("VROBBLER_LASTFM_SECRET_KEY") From 257e6899d3a0d1eb4f0d2eeec035f40bdcd9680d Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 7 Apr 2025 13:40:32 -0400 Subject: [PATCH 09/14] [tests] Clean up podcast tests --- tests/scrobbles_tests/conftest.py | 6 ++-- tests/scrobbles_tests/test_views.py | 56 +++-------------------------- vrobbler/apps/podcasts/models.py | 10 +++--- 3 files changed, 15 insertions(+), 57 deletions(-) diff --git a/tests/scrobbles_tests/conftest.py b/tests/scrobbles_tests/conftest.py index 488393d..627eabd 100644 --- a/tests/scrobbles_tests/conftest.py +++ b/tests/scrobbles_tests/conftest.py @@ -108,9 +108,11 @@ def mopidy_track_diff_album_request_data(**kwargs): @pytest.fixture -def mopidy_podcast(): +def mopidy_podcast_request_data(): mopidy_uri = "local:podcast:Up%20First/2022-01-01%20Up%20First.mp3" - return MopidyRequest(mopidy_uri=mopidy_uri) + return MopidyRequest( + mopidy_uri=mopidy_uri, artist="NPR", album="Up First" + ).request_json class JellyfinTrackRequest: diff --git a/tests/scrobbles_tests/test_views.py b/tests/scrobbles_tests/test_views.py index 6294ab2..d5041ac 100644 --- a/tests/scrobbles_tests/test_views.py +++ b/tests/scrobbles_tests/test_views.py @@ -30,53 +30,6 @@ def test_bad_mopidy_request_data(client, valid_auth_token): ) -@pytest.mark.parametrize( - "seconds, expected_percent_played, expected_scrobble_id", - [ - (1, 1, 1), - (58, 96, 1), - (59, 98, 1), - (60, 100, 1), - (1, 1, 1), - (1, 1, 1), - ], -) -@pytest.mark.django_db -def test_scrobble_mopidy_track( - client, - mopidy_track, - valid_auth_token, - seconds, - expected_percent_played, - expected_scrobble_id, -): - url = reverse("scrobbles:mopidy-webhook") - headers = {"Authorization": f"Token {valid_auth_token}"} - - # Start new scrobble - minutes = 0 - calc_seconds = seconds - if seconds >= 60: - minutes = 1 - calc_seconds = calc_seconds % 10 - with time_machine.travel(datetime(2024, 1, 14, 12, minutes, calc_seconds)): - mopidy_track.request_data["playback_time_ticks"] = seconds * 1000 - response = client.post( - url, - mopidy_track.request_json, - content_type="application/json", - headers=headers, - ) - assert response.status_code == 200 - assert response.data == {"scrobble_id": expected_scrobble_id} - - scrobble = Scrobble.objects.get(id=1) - assert scrobble.percent_played == expected_percent_played - assert scrobble.media_obj.__class__ == Track - assert scrobble.media_obj.title == "Same in the End" - - -# @pytest.mark.skip(reason="Allmusic API is unstable") @pytest.mark.django_db @patch("music.utils.lookup_artist_from_mb", return_value={}) @patch( @@ -128,12 +81,13 @@ def test_scrobble_mopidy_same_track_different_album( assert scrobble.media_obj.title == "Same in the End" -@pytest.mark.skip( - "Need to add a mock podcast request data, tho Google Podcasts is gone :thinking:" -) @pytest.mark.django_db +@patch( + "podcasts.sources.podcastindex.lookup_podcast_from_podcastindex", + return_value={}, +) def test_scrobble_mopidy_podcast( - client, mopidy_podcast_request_data, valid_auth_token + mock_lookup_podcast, client, mopidy_podcast_request_data, valid_auth_token ): url = reverse("scrobbles:mopidy-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} diff --git a/vrobbler/apps/podcasts/models.py b/vrobbler/apps/podcasts/models.py index b046b88..685b6bc 100644 --- a/vrobbler/apps/podcasts/models.py +++ b/vrobbler/apps/podcasts/models.py @@ -9,6 +9,7 @@ from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django_extensions.db.models import TimeStampedModel +from podcasts.sources.podcastindex import lookup_podcast_from_podcastindex from scrobbles.mixins import ( ObjectWithGenres, ScrobblableConstants, @@ -16,10 +17,6 @@ from scrobbles.mixins import ( ) from taggit.managers import TaggableManager -from podcasts.sources.podcastindex import ( - lookup_podcast_from_podcastindex, -) - logger = logging.getLogger(__name__) BNULL = {"blank": True, "null": True} @@ -115,6 +112,11 @@ class PodcastEpisode(ScrobblableMixin): pub_date = models.DateField(**BNULL) mopidy_uri = models.CharField(max_length=255, **BNULL) + def get_absolute_url(self): + return reverse( + "podcasts:podcast_detail", kwargs={"slug": self.podcast.uuid} + ) + def __str__(self): return f"{self.title}" From 484be0a64e230918c1f184e0d7542a1699b6faf1 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 7 Apr 2025 13:45:13 -0400 Subject: [PATCH 10/14] [tests] Skip openlibrary lookup tests --- Makefile | 2 ++ vrobbler/apps/books/tests/test_openlibrary.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Makefile b/Makefile index dfe923a..9f4bdf8 100644 --- a/Makefile +++ b/Makefile @@ -2,3 +2,5 @@ deploy: ssh vrobbler.service "rm -rf /usr/local/lib/python3.11/site-packages/vrobbler-0.15.4.dist-info/ && pip install git+https://code.unbl.ink/secstate/vrobbler.git@develop && immortalctl restart vrobbler && immortalctl restart vrobbler-celery && vrobbler migrate" logs: ssh life.unbl.ink tail -n 100 -f /var/log/vrobbler.json +test: + pytest vrobbler diff --git a/vrobbler/apps/books/tests/test_openlibrary.py b/vrobbler/apps/books/tests/test_openlibrary.py index 369de7f..1483b9d 100644 --- a/vrobbler/apps/books/tests/test_openlibrary.py +++ b/vrobbler/apps/books/tests/test_openlibrary.py @@ -5,6 +5,7 @@ import pytest from books.openlibrary import lookup_book_from_openlibrary +@pytest.mark.skip() def test_lookup_modern_book(): book = lookup_book_from_openlibrary("Matrix", "Lauren Groff") assert book.get("title") == "Matrix" @@ -12,6 +13,7 @@ def test_lookup_modern_book(): assert book.get("ol_author_id") == "OL3675729A" +@pytest.mark.skip() def test_lookup_classic_book(): book = lookup_book_from_openlibrary( "The Life of Castruccio Castracani", "Machiavelli" @@ -21,6 +23,7 @@ def test_lookup_classic_book(): assert book.get("ol_author_id") == "OL23135A" +@pytest.mark.skip() def test_lookup_foreign_book(): book = lookup_book_from_openlibrary("Ravagé", "René Barjavel") assert book.get("title") == "Ravage" From 4b5281bdd83e1e0038bc356e35ccd4ce271ba1c1 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 7 Apr 2025 13:47:21 -0400 Subject: [PATCH 11/14] [cleaning] Adding migrations that are past due --- .../apps/books/migrations/0028_delete_page.py | 16 + .../0023_alter_userprofile_timezone.py | 601 ++++++++++++++++++ 2 files changed, 617 insertions(+) create mode 100644 vrobbler/apps/books/migrations/0028_delete_page.py create mode 100644 vrobbler/apps/profiles/migrations/0023_alter_userprofile_timezone.py diff --git a/vrobbler/apps/books/migrations/0028_delete_page.py b/vrobbler/apps/books/migrations/0028_delete_page.py new file mode 100644 index 0000000..9ebffbe --- /dev/null +++ b/vrobbler/apps/books/migrations/0028_delete_page.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.19 on 2025-04-07 17:16 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("books", "0027_remove_paper_num_citations_paper_tldr"), + ] + + operations = [ + migrations.DeleteModel( + name="Page", + ), + ] diff --git a/vrobbler/apps/profiles/migrations/0023_alter_userprofile_timezone.py b/vrobbler/apps/profiles/migrations/0023_alter_userprofile_timezone.py new file mode 100644 index 0000000..7134a45 --- /dev/null +++ b/vrobbler/apps/profiles/migrations/0023_alter_userprofile_timezone.py @@ -0,0 +1,601 @@ +# Generated by Django 4.2.19 on 2025-04-07 17:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("profiles", "0022_userprofile_task_context_tags_str_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="userprofile", + name="timezone", + field=models.CharField( + choices=[ + ("Pacific/Midway", "(GMT-1100) Pacific/Midway"), + ("Pacific/Niue", "(GMT-1100) Pacific/Niue"), + ("Pacific/Pago_Pago", "(GMT-1100) Pacific/Pago_Pago"), + ("Pacific/Honolulu", "(GMT-1000) Pacific/Honolulu"), + ("Pacific/Rarotonga", "(GMT-1000) Pacific/Rarotonga"), + ("Pacific/Tahiti", "(GMT-1000) Pacific/Tahiti"), + ("US/Hawaii", "(GMT-1000) US/Hawaii"), + ("Pacific/Marquesas", "(GMT-0930) Pacific/Marquesas"), + ("America/Adak", "(GMT-0900) America/Adak"), + ("Pacific/Gambier", "(GMT-0900) Pacific/Gambier"), + ("America/Anchorage", "(GMT-0800) America/Anchorage"), + ("America/Juneau", "(GMT-0800) America/Juneau"), + ("America/Metlakatla", "(GMT-0800) America/Metlakatla"), + ("America/Nome", "(GMT-0800) America/Nome"), + ("America/Sitka", "(GMT-0800) America/Sitka"), + ("America/Yakutat", "(GMT-0800) America/Yakutat"), + ("Pacific/Pitcairn", "(GMT-0800) Pacific/Pitcairn"), + ("US/Alaska", "(GMT-0800) US/Alaska"), + ("America/Creston", "(GMT-0700) America/Creston"), + ("America/Dawson", "(GMT-0700) America/Dawson"), + ( + "America/Dawson_Creek", + "(GMT-0700) America/Dawson_Creek", + ), + ("America/Fort_Nelson", "(GMT-0700) America/Fort_Nelson"), + ("America/Hermosillo", "(GMT-0700) America/Hermosillo"), + ("America/Los_Angeles", "(GMT-0700) America/Los_Angeles"), + ("America/Mazatlan", "(GMT-0700) America/Mazatlan"), + ("America/Phoenix", "(GMT-0700) America/Phoenix"), + ("America/Tijuana", "(GMT-0700) America/Tijuana"), + ("America/Vancouver", "(GMT-0700) America/Vancouver"), + ("America/Whitehorse", "(GMT-0700) America/Whitehorse"), + ("Canada/Pacific", "(GMT-0700) Canada/Pacific"), + ("US/Arizona", "(GMT-0700) US/Arizona"), + ("US/Pacific", "(GMT-0700) US/Pacific"), + ( + "America/Bahia_Banderas", + "(GMT-0600) America/Bahia_Banderas", + ), + ("America/Belize", "(GMT-0600) America/Belize"), + ("America/Boise", "(GMT-0600) America/Boise"), + ( + "America/Cambridge_Bay", + "(GMT-0600) America/Cambridge_Bay", + ), + ("America/Chihuahua", "(GMT-0600) America/Chihuahua"), + ( + "America/Ciudad_Juarez", + "(GMT-0600) America/Ciudad_Juarez", + ), + ("America/Costa_Rica", "(GMT-0600) America/Costa_Rica"), + ("America/Denver", "(GMT-0600) America/Denver"), + ("America/Edmonton", "(GMT-0600) America/Edmonton"), + ("America/El_Salvador", "(GMT-0600) America/El_Salvador"), + ("America/Guatemala", "(GMT-0600) America/Guatemala"), + ("America/Inuvik", "(GMT-0600) America/Inuvik"), + ("America/Managua", "(GMT-0600) America/Managua"), + ("America/Merida", "(GMT-0600) America/Merida"), + ("America/Mexico_City", "(GMT-0600) America/Mexico_City"), + ("America/Monterrey", "(GMT-0600) America/Monterrey"), + ("America/Regina", "(GMT-0600) America/Regina"), + ( + "America/Swift_Current", + "(GMT-0600) America/Swift_Current", + ), + ("America/Tegucigalpa", "(GMT-0600) America/Tegucigalpa"), + ("America/Yellowknife", "(GMT-0600) America/Yellowknife"), + ("Canada/Mountain", "(GMT-0600) Canada/Mountain"), + ("Pacific/Easter", "(GMT-0600) Pacific/Easter"), + ("Pacific/Galapagos", "(GMT-0600) Pacific/Galapagos"), + ("US/Mountain", "(GMT-0600) US/Mountain"), + ("America/Atikokan", "(GMT-0500) America/Atikokan"), + ("America/Bogota", "(GMT-0500) America/Bogota"), + ("America/Cancun", "(GMT-0500) America/Cancun"), + ("America/Cayman", "(GMT-0500) America/Cayman"), + ("America/Chicago", "(GMT-0500) America/Chicago"), + ("America/Eirunepe", "(GMT-0500) America/Eirunepe"), + ("America/Guayaquil", "(GMT-0500) America/Guayaquil"), + ( + "America/Indiana/Knox", + "(GMT-0500) America/Indiana/Knox", + ), + ( + "America/Indiana/Tell_City", + "(GMT-0500) America/Indiana/Tell_City", + ), + ("America/Jamaica", "(GMT-0500) America/Jamaica"), + ("America/Lima", "(GMT-0500) America/Lima"), + ("America/Matamoros", "(GMT-0500) America/Matamoros"), + ("America/Menominee", "(GMT-0500) America/Menominee"), + ( + "America/North_Dakota/Beulah", + "(GMT-0500) America/North_Dakota/Beulah", + ), + ( + "America/North_Dakota/Center", + "(GMT-0500) America/North_Dakota/Center", + ), + ( + "America/North_Dakota/New_Salem", + "(GMT-0500) America/North_Dakota/New_Salem", + ), + ("America/Ojinaga", "(GMT-0500) America/Ojinaga"), + ("America/Panama", "(GMT-0500) America/Panama"), + ( + "America/Rankin_Inlet", + "(GMT-0500) America/Rankin_Inlet", + ), + ("America/Resolute", "(GMT-0500) America/Resolute"), + ("America/Rio_Branco", "(GMT-0500) America/Rio_Branco"), + ("America/Winnipeg", "(GMT-0500) America/Winnipeg"), + ("Canada/Central", "(GMT-0500) Canada/Central"), + ("US/Central", "(GMT-0500) US/Central"), + ("America/Anguilla", "(GMT-0400) America/Anguilla"), + ("America/Antigua", "(GMT-0400) America/Antigua"), + ("America/Aruba", "(GMT-0400) America/Aruba"), + ("America/Asuncion", "(GMT-0400) America/Asuncion"), + ("America/Barbados", "(GMT-0400) America/Barbados"), + ( + "America/Blanc-Sablon", + "(GMT-0400) America/Blanc-Sablon", + ), + ("America/Boa_Vista", "(GMT-0400) America/Boa_Vista"), + ( + "America/Campo_Grande", + "(GMT-0400) America/Campo_Grande", + ), + ("America/Caracas", "(GMT-0400) America/Caracas"), + ("America/Cuiaba", "(GMT-0400) America/Cuiaba"), + ("America/Curacao", "(GMT-0400) America/Curacao"), + ("America/Detroit", "(GMT-0400) America/Detroit"), + ("America/Dominica", "(GMT-0400) America/Dominica"), + ("America/Grand_Turk", "(GMT-0400) America/Grand_Turk"), + ("America/Grenada", "(GMT-0400) America/Grenada"), + ("America/Guadeloupe", "(GMT-0400) America/Guadeloupe"), + ("America/Guyana", "(GMT-0400) America/Guyana"), + ("America/Havana", "(GMT-0400) America/Havana"), + ( + "America/Indiana/Indianapolis", + "(GMT-0400) America/Indiana/Indianapolis", + ), + ( + "America/Indiana/Marengo", + "(GMT-0400) America/Indiana/Marengo", + ), + ( + "America/Indiana/Petersburg", + "(GMT-0400) America/Indiana/Petersburg", + ), + ( + "America/Indiana/Vevay", + "(GMT-0400) America/Indiana/Vevay", + ), + ( + "America/Indiana/Vincennes", + "(GMT-0400) America/Indiana/Vincennes", + ), + ( + "America/Indiana/Winamac", + "(GMT-0400) America/Indiana/Winamac", + ), + ("America/Iqaluit", "(GMT-0400) America/Iqaluit"), + ( + "America/Kentucky/Louisville", + "(GMT-0400) America/Kentucky/Louisville", + ), + ( + "America/Kentucky/Monticello", + "(GMT-0400) America/Kentucky/Monticello", + ), + ("America/Kralendijk", "(GMT-0400) America/Kralendijk"), + ("America/La_Paz", "(GMT-0400) America/La_Paz"), + ( + "America/Lower_Princes", + "(GMT-0400) America/Lower_Princes", + ), + ("America/Manaus", "(GMT-0400) America/Manaus"), + ("America/Marigot", "(GMT-0400) America/Marigot"), + ("America/Martinique", "(GMT-0400) America/Martinique"), + ("America/Montserrat", "(GMT-0400) America/Montserrat"), + ("America/Nassau", "(GMT-0400) America/Nassau"), + ("America/New_York", "(GMT-0400) America/New_York"), + ( + "America/Port-au-Prince", + "(GMT-0400) America/Port-au-Prince", + ), + ( + "America/Port_of_Spain", + "(GMT-0400) America/Port_of_Spain", + ), + ("America/Porto_Velho", "(GMT-0400) America/Porto_Velho"), + ("America/Puerto_Rico", "(GMT-0400) America/Puerto_Rico"), + ("America/Santiago", "(GMT-0400) America/Santiago"), + ( + "America/Santo_Domingo", + "(GMT-0400) America/Santo_Domingo", + ), + ( + "America/St_Barthelemy", + "(GMT-0400) America/St_Barthelemy", + ), + ("America/St_Kitts", "(GMT-0400) America/St_Kitts"), + ("America/St_Lucia", "(GMT-0400) America/St_Lucia"), + ("America/St_Thomas", "(GMT-0400) America/St_Thomas"), + ("America/St_Vincent", "(GMT-0400) America/St_Vincent"), + ("America/Toronto", "(GMT-0400) America/Toronto"), + ("America/Tortola", "(GMT-0400) America/Tortola"), + ("Canada/Eastern", "(GMT-0400) Canada/Eastern"), + ("US/Eastern", "(GMT-0400) US/Eastern"), + ("America/Araguaina", "(GMT-0300) America/Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "(GMT-0300) America/Argentina/Buenos_Aires", + ), + ( + "America/Argentina/Catamarca", + "(GMT-0300) America/Argentina/Catamarca", + ), + ( + "America/Argentina/Cordoba", + "(GMT-0300) America/Argentina/Cordoba", + ), + ( + "America/Argentina/Jujuy", + "(GMT-0300) America/Argentina/Jujuy", + ), + ( + "America/Argentina/La_Rioja", + "(GMT-0300) America/Argentina/La_Rioja", + ), + ( + "America/Argentina/Mendoza", + "(GMT-0300) America/Argentina/Mendoza", + ), + ( + "America/Argentina/Rio_Gallegos", + "(GMT-0300) America/Argentina/Rio_Gallegos", + ), + ( + "America/Argentina/Salta", + "(GMT-0300) America/Argentina/Salta", + ), + ( + "America/Argentina/San_Juan", + "(GMT-0300) America/Argentina/San_Juan", + ), + ( + "America/Argentina/San_Luis", + "(GMT-0300) America/Argentina/San_Luis", + ), + ( + "America/Argentina/Tucuman", + "(GMT-0300) America/Argentina/Tucuman", + ), + ( + "America/Argentina/Ushuaia", + "(GMT-0300) America/Argentina/Ushuaia", + ), + ("America/Bahia", "(GMT-0300) America/Bahia"), + ("America/Belem", "(GMT-0300) America/Belem"), + ("America/Cayenne", "(GMT-0300) America/Cayenne"), + ("America/Fortaleza", "(GMT-0300) America/Fortaleza"), + ("America/Glace_Bay", "(GMT-0300) America/Glace_Bay"), + ("America/Goose_Bay", "(GMT-0300) America/Goose_Bay"), + ("America/Halifax", "(GMT-0300) America/Halifax"), + ("America/Maceio", "(GMT-0300) America/Maceio"), + ("America/Moncton", "(GMT-0300) America/Moncton"), + ("America/Montevideo", "(GMT-0300) America/Montevideo"), + ("America/Paramaribo", "(GMT-0300) America/Paramaribo"), + ( + "America/Punta_Arenas", + "(GMT-0300) America/Punta_Arenas", + ), + ("America/Recife", "(GMT-0300) America/Recife"), + ("America/Santarem", "(GMT-0300) America/Santarem"), + ("America/Sao_Paulo", "(GMT-0300) America/Sao_Paulo"), + ("America/Thule", "(GMT-0300) America/Thule"), + ("Antarctica/Palmer", "(GMT-0300) Antarctica/Palmer"), + ("Antarctica/Rothera", "(GMT-0300) Antarctica/Rothera"), + ("Atlantic/Bermuda", "(GMT-0300) Atlantic/Bermuda"), + ("Atlantic/Stanley", "(GMT-0300) Atlantic/Stanley"), + ("Canada/Atlantic", "(GMT-0300) Canada/Atlantic"), + ("America/St_Johns", "(GMT-0230) America/St_Johns"), + ("Canada/Newfoundland", "(GMT-0230) Canada/Newfoundland"), + ("America/Miquelon", "(GMT-0200) America/Miquelon"), + ("America/Noronha", "(GMT-0200) America/Noronha"), + ("America/Nuuk", "(GMT-0200) America/Nuuk"), + ( + "Atlantic/South_Georgia", + "(GMT-0200) Atlantic/South_Georgia", + ), + ("Atlantic/Cape_Verde", "(GMT-0100) Atlantic/Cape_Verde"), + ("Africa/Abidjan", "(GMT+0000) Africa/Abidjan"), + ("Africa/Accra", "(GMT+0000) Africa/Accra"), + ("Africa/Bamako", "(GMT+0000) Africa/Bamako"), + ("Africa/Banjul", "(GMT+0000) Africa/Banjul"), + ("Africa/Bissau", "(GMT+0000) Africa/Bissau"), + ("Africa/Conakry", "(GMT+0000) Africa/Conakry"), + ("Africa/Dakar", "(GMT+0000) Africa/Dakar"), + ("Africa/Freetown", "(GMT+0000) Africa/Freetown"), + ("Africa/Lome", "(GMT+0000) Africa/Lome"), + ("Africa/Monrovia", "(GMT+0000) Africa/Monrovia"), + ("Africa/Nouakchott", "(GMT+0000) Africa/Nouakchott"), + ("Africa/Ouagadougou", "(GMT+0000) Africa/Ouagadougou"), + ("Africa/Sao_Tome", "(GMT+0000) Africa/Sao_Tome"), + ( + "America/Danmarkshavn", + "(GMT+0000) America/Danmarkshavn", + ), + ( + "America/Scoresbysund", + "(GMT+0000) America/Scoresbysund", + ), + ("Atlantic/Azores", "(GMT+0000) Atlantic/Azores"), + ("Atlantic/Reykjavik", "(GMT+0000) Atlantic/Reykjavik"), + ("Atlantic/St_Helena", "(GMT+0000) Atlantic/St_Helena"), + ("GMT", "(GMT+0000) GMT"), + ("UTC", "(GMT+0000) UTC"), + ("Africa/Algiers", "(GMT+0100) Africa/Algiers"), + ("Africa/Bangui", "(GMT+0100) Africa/Bangui"), + ("Africa/Brazzaville", "(GMT+0100) Africa/Brazzaville"), + ("Africa/Casablanca", "(GMT+0100) Africa/Casablanca"), + ("Africa/Douala", "(GMT+0100) Africa/Douala"), + ("Africa/El_Aaiun", "(GMT+0100) Africa/El_Aaiun"), + ("Africa/Kinshasa", "(GMT+0100) Africa/Kinshasa"), + ("Africa/Lagos", "(GMT+0100) Africa/Lagos"), + ("Africa/Libreville", "(GMT+0100) Africa/Libreville"), + ("Africa/Luanda", "(GMT+0100) Africa/Luanda"), + ("Africa/Malabo", "(GMT+0100) Africa/Malabo"), + ("Africa/Ndjamena", "(GMT+0100) Africa/Ndjamena"), + ("Africa/Niamey", "(GMT+0100) Africa/Niamey"), + ("Africa/Porto-Novo", "(GMT+0100) Africa/Porto-Novo"), + ("Africa/Tunis", "(GMT+0100) Africa/Tunis"), + ("Atlantic/Canary", "(GMT+0100) Atlantic/Canary"), + ("Atlantic/Faroe", "(GMT+0100) Atlantic/Faroe"), + ("Atlantic/Madeira", "(GMT+0100) Atlantic/Madeira"), + ("Europe/Dublin", "(GMT+0100) Europe/Dublin"), + ("Europe/Guernsey", "(GMT+0100) Europe/Guernsey"), + ("Europe/Isle_of_Man", "(GMT+0100) Europe/Isle_of_Man"), + ("Europe/Jersey", "(GMT+0100) Europe/Jersey"), + ("Europe/Lisbon", "(GMT+0100) Europe/Lisbon"), + ("Europe/London", "(GMT+0100) Europe/London"), + ("Africa/Blantyre", "(GMT+0200) Africa/Blantyre"), + ("Africa/Bujumbura", "(GMT+0200) Africa/Bujumbura"), + ("Africa/Cairo", "(GMT+0200) Africa/Cairo"), + ("Africa/Ceuta", "(GMT+0200) Africa/Ceuta"), + ("Africa/Gaborone", "(GMT+0200) Africa/Gaborone"), + ("Africa/Harare", "(GMT+0200) Africa/Harare"), + ("Africa/Johannesburg", "(GMT+0200) Africa/Johannesburg"), + ("Africa/Juba", "(GMT+0200) Africa/Juba"), + ("Africa/Khartoum", "(GMT+0200) Africa/Khartoum"), + ("Africa/Kigali", "(GMT+0200) Africa/Kigali"), + ("Africa/Lubumbashi", "(GMT+0200) Africa/Lubumbashi"), + ("Africa/Lusaka", "(GMT+0200) Africa/Lusaka"), + ("Africa/Maputo", "(GMT+0200) Africa/Maputo"), + ("Africa/Maseru", "(GMT+0200) Africa/Maseru"), + ("Africa/Mbabane", "(GMT+0200) Africa/Mbabane"), + ("Africa/Tripoli", "(GMT+0200) Africa/Tripoli"), + ("Africa/Windhoek", "(GMT+0200) Africa/Windhoek"), + ("Antarctica/Troll", "(GMT+0200) Antarctica/Troll"), + ("Arctic/Longyearbyen", "(GMT+0200) Arctic/Longyearbyen"), + ("Europe/Amsterdam", "(GMT+0200) Europe/Amsterdam"), + ("Europe/Andorra", "(GMT+0200) Europe/Andorra"), + ("Europe/Belgrade", "(GMT+0200) Europe/Belgrade"), + ("Europe/Berlin", "(GMT+0200) Europe/Berlin"), + ("Europe/Bratislava", "(GMT+0200) Europe/Bratislava"), + ("Europe/Brussels", "(GMT+0200) Europe/Brussels"), + ("Europe/Budapest", "(GMT+0200) Europe/Budapest"), + ("Europe/Busingen", "(GMT+0200) Europe/Busingen"), + ("Europe/Copenhagen", "(GMT+0200) Europe/Copenhagen"), + ("Europe/Gibraltar", "(GMT+0200) Europe/Gibraltar"), + ("Europe/Kaliningrad", "(GMT+0200) Europe/Kaliningrad"), + ("Europe/Ljubljana", "(GMT+0200) Europe/Ljubljana"), + ("Europe/Luxembourg", "(GMT+0200) Europe/Luxembourg"), + ("Europe/Madrid", "(GMT+0200) Europe/Madrid"), + ("Europe/Malta", "(GMT+0200) Europe/Malta"), + ("Europe/Monaco", "(GMT+0200) Europe/Monaco"), + ("Europe/Oslo", "(GMT+0200) Europe/Oslo"), + ("Europe/Paris", "(GMT+0200) Europe/Paris"), + ("Europe/Podgorica", "(GMT+0200) Europe/Podgorica"), + ("Europe/Prague", "(GMT+0200) Europe/Prague"), + ("Europe/Rome", "(GMT+0200) Europe/Rome"), + ("Europe/San_Marino", "(GMT+0200) Europe/San_Marino"), + ("Europe/Sarajevo", "(GMT+0200) Europe/Sarajevo"), + ("Europe/Skopje", "(GMT+0200) Europe/Skopje"), + ("Europe/Stockholm", "(GMT+0200) Europe/Stockholm"), + ("Europe/Tirane", "(GMT+0200) Europe/Tirane"), + ("Europe/Vaduz", "(GMT+0200) Europe/Vaduz"), + ("Europe/Vatican", "(GMT+0200) Europe/Vatican"), + ("Europe/Vienna", "(GMT+0200) Europe/Vienna"), + ("Europe/Warsaw", "(GMT+0200) Europe/Warsaw"), + ("Europe/Zagreb", "(GMT+0200) Europe/Zagreb"), + ("Europe/Zurich", "(GMT+0200) Europe/Zurich"), + ("Africa/Addis_Ababa", "(GMT+0300) Africa/Addis_Ababa"), + ("Africa/Asmara", "(GMT+0300) Africa/Asmara"), + ( + "Africa/Dar_es_Salaam", + "(GMT+0300) Africa/Dar_es_Salaam", + ), + ("Africa/Djibouti", "(GMT+0300) Africa/Djibouti"), + ("Africa/Kampala", "(GMT+0300) Africa/Kampala"), + ("Africa/Mogadishu", "(GMT+0300) Africa/Mogadishu"), + ("Africa/Nairobi", "(GMT+0300) Africa/Nairobi"), + ("Antarctica/Syowa", "(GMT+0300) Antarctica/Syowa"), + ("Asia/Aden", "(GMT+0300) Asia/Aden"), + ("Asia/Amman", "(GMT+0300) Asia/Amman"), + ("Asia/Baghdad", "(GMT+0300) Asia/Baghdad"), + ("Asia/Bahrain", "(GMT+0300) Asia/Bahrain"), + ("Asia/Beirut", "(GMT+0300) Asia/Beirut"), + ("Asia/Damascus", "(GMT+0300) Asia/Damascus"), + ("Asia/Famagusta", "(GMT+0300) Asia/Famagusta"), + ("Asia/Gaza", "(GMT+0300) Asia/Gaza"), + ("Asia/Hebron", "(GMT+0300) Asia/Hebron"), + ("Asia/Jerusalem", "(GMT+0300) Asia/Jerusalem"), + ("Asia/Kuwait", "(GMT+0300) Asia/Kuwait"), + ("Asia/Nicosia", "(GMT+0300) Asia/Nicosia"), + ("Asia/Qatar", "(GMT+0300) Asia/Qatar"), + ("Asia/Riyadh", "(GMT+0300) Asia/Riyadh"), + ("Europe/Athens", "(GMT+0300) Europe/Athens"), + ("Europe/Bucharest", "(GMT+0300) Europe/Bucharest"), + ("Europe/Chisinau", "(GMT+0300) Europe/Chisinau"), + ("Europe/Helsinki", "(GMT+0300) Europe/Helsinki"), + ("Europe/Istanbul", "(GMT+0300) Europe/Istanbul"), + ("Europe/Kirov", "(GMT+0300) Europe/Kirov"), + ("Europe/Kyiv", "(GMT+0300) Europe/Kyiv"), + ("Europe/Mariehamn", "(GMT+0300) Europe/Mariehamn"), + ("Europe/Minsk", "(GMT+0300) Europe/Minsk"), + ("Europe/Moscow", "(GMT+0300) Europe/Moscow"), + ("Europe/Riga", "(GMT+0300) Europe/Riga"), + ("Europe/Simferopol", "(GMT+0300) Europe/Simferopol"), + ("Europe/Sofia", "(GMT+0300) Europe/Sofia"), + ("Europe/Tallinn", "(GMT+0300) Europe/Tallinn"), + ("Europe/Vilnius", "(GMT+0300) Europe/Vilnius"), + ("Europe/Volgograd", "(GMT+0300) Europe/Volgograd"), + ("Indian/Antananarivo", "(GMT+0300) Indian/Antananarivo"), + ("Indian/Comoro", "(GMT+0300) Indian/Comoro"), + ("Indian/Mayotte", "(GMT+0300) Indian/Mayotte"), + ("Asia/Tehran", "(GMT+0330) Asia/Tehran"), + ("Asia/Baku", "(GMT+0400) Asia/Baku"), + ("Asia/Dubai", "(GMT+0400) Asia/Dubai"), + ("Asia/Muscat", "(GMT+0400) Asia/Muscat"), + ("Asia/Tbilisi", "(GMT+0400) Asia/Tbilisi"), + ("Asia/Yerevan", "(GMT+0400) Asia/Yerevan"), + ("Europe/Astrakhan", "(GMT+0400) Europe/Astrakhan"), + ("Europe/Samara", "(GMT+0400) Europe/Samara"), + ("Europe/Saratov", "(GMT+0400) Europe/Saratov"), + ("Europe/Ulyanovsk", "(GMT+0400) Europe/Ulyanovsk"), + ("Indian/Mahe", "(GMT+0400) Indian/Mahe"), + ("Indian/Mauritius", "(GMT+0400) Indian/Mauritius"), + ("Indian/Reunion", "(GMT+0400) Indian/Reunion"), + ("Asia/Kabul", "(GMT+0430) Asia/Kabul"), + ("Antarctica/Mawson", "(GMT+0500) Antarctica/Mawson"), + ("Asia/Aqtau", "(GMT+0500) Asia/Aqtau"), + ("Asia/Aqtobe", "(GMT+0500) Asia/Aqtobe"), + ("Asia/Ashgabat", "(GMT+0500) Asia/Ashgabat"), + ("Asia/Atyrau", "(GMT+0500) Asia/Atyrau"), + ("Asia/Dushanbe", "(GMT+0500) Asia/Dushanbe"), + ("Asia/Karachi", "(GMT+0500) Asia/Karachi"), + ("Asia/Oral", "(GMT+0500) Asia/Oral"), + ("Asia/Qyzylorda", "(GMT+0500) Asia/Qyzylorda"), + ("Asia/Samarkand", "(GMT+0500) Asia/Samarkand"), + ("Asia/Tashkent", "(GMT+0500) Asia/Tashkent"), + ("Asia/Yekaterinburg", "(GMT+0500) Asia/Yekaterinburg"), + ("Indian/Kerguelen", "(GMT+0500) Indian/Kerguelen"), + ("Indian/Maldives", "(GMT+0500) Indian/Maldives"), + ("Asia/Colombo", "(GMT+0530) Asia/Colombo"), + ("Asia/Kolkata", "(GMT+0530) Asia/Kolkata"), + ("Asia/Kathmandu", "(GMT+0545) Asia/Kathmandu"), + ("Antarctica/Vostok", "(GMT+0600) Antarctica/Vostok"), + ("Asia/Almaty", "(GMT+0600) Asia/Almaty"), + ("Asia/Bishkek", "(GMT+0600) Asia/Bishkek"), + ("Asia/Dhaka", "(GMT+0600) Asia/Dhaka"), + ("Asia/Omsk", "(GMT+0600) Asia/Omsk"), + ("Asia/Qostanay", "(GMT+0600) Asia/Qostanay"), + ("Asia/Thimphu", "(GMT+0600) Asia/Thimphu"), + ("Asia/Urumqi", "(GMT+0600) Asia/Urumqi"), + ("Indian/Chagos", "(GMT+0600) Indian/Chagos"), + ("Asia/Yangon", "(GMT+0630) Asia/Yangon"), + ("Indian/Cocos", "(GMT+0630) Indian/Cocos"), + ("Antarctica/Davis", "(GMT+0700) Antarctica/Davis"), + ("Asia/Bangkok", "(GMT+0700) Asia/Bangkok"), + ("Asia/Barnaul", "(GMT+0700) Asia/Barnaul"), + ("Asia/Ho_Chi_Minh", "(GMT+0700) Asia/Ho_Chi_Minh"), + ("Asia/Hovd", "(GMT+0700) Asia/Hovd"), + ("Asia/Jakarta", "(GMT+0700) Asia/Jakarta"), + ("Asia/Krasnoyarsk", "(GMT+0700) Asia/Krasnoyarsk"), + ("Asia/Novokuznetsk", "(GMT+0700) Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "(GMT+0700) Asia/Novosibirsk"), + ("Asia/Phnom_Penh", "(GMT+0700) Asia/Phnom_Penh"), + ("Asia/Pontianak", "(GMT+0700) Asia/Pontianak"), + ("Asia/Tomsk", "(GMT+0700) Asia/Tomsk"), + ("Asia/Vientiane", "(GMT+0700) Asia/Vientiane"), + ("Indian/Christmas", "(GMT+0700) Indian/Christmas"), + ("Asia/Brunei", "(GMT+0800) Asia/Brunei"), + ("Asia/Choibalsan", "(GMT+0800) Asia/Choibalsan"), + ("Asia/Hong_Kong", "(GMT+0800) Asia/Hong_Kong"), + ("Asia/Irkutsk", "(GMT+0800) Asia/Irkutsk"), + ("Asia/Kuala_Lumpur", "(GMT+0800) Asia/Kuala_Lumpur"), + ("Asia/Kuching", "(GMT+0800) Asia/Kuching"), + ("Asia/Macau", "(GMT+0800) Asia/Macau"), + ("Asia/Makassar", "(GMT+0800) Asia/Makassar"), + ("Asia/Manila", "(GMT+0800) Asia/Manila"), + ("Asia/Shanghai", "(GMT+0800) Asia/Shanghai"), + ("Asia/Singapore", "(GMT+0800) Asia/Singapore"), + ("Asia/Taipei", "(GMT+0800) Asia/Taipei"), + ("Asia/Ulaanbaatar", "(GMT+0800) Asia/Ulaanbaatar"), + ("Australia/Perth", "(GMT+0800) Australia/Perth"), + ("Australia/Eucla", "(GMT+0845) Australia/Eucla"), + ("Asia/Chita", "(GMT+0900) Asia/Chita"), + ("Asia/Dili", "(GMT+0900) Asia/Dili"), + ("Asia/Jayapura", "(GMT+0900) Asia/Jayapura"), + ("Asia/Khandyga", "(GMT+0900) Asia/Khandyga"), + ("Asia/Pyongyang", "(GMT+0900) Asia/Pyongyang"), + ("Asia/Seoul", "(GMT+0900) Asia/Seoul"), + ("Asia/Tokyo", "(GMT+0900) Asia/Tokyo"), + ("Asia/Yakutsk", "(GMT+0900) Asia/Yakutsk"), + ("Pacific/Palau", "(GMT+0900) Pacific/Palau"), + ("Australia/Adelaide", "(GMT+0930) Australia/Adelaide"), + ( + "Australia/Broken_Hill", + "(GMT+0930) Australia/Broken_Hill", + ), + ("Australia/Darwin", "(GMT+0930) Australia/Darwin"), + ( + "Antarctica/DumontDUrville", + "(GMT+1000) Antarctica/DumontDUrville", + ), + ( + "Antarctica/Macquarie", + "(GMT+1000) Antarctica/Macquarie", + ), + ("Asia/Ust-Nera", "(GMT+1000) Asia/Ust-Nera"), + ("Asia/Vladivostok", "(GMT+1000) Asia/Vladivostok"), + ("Australia/Brisbane", "(GMT+1000) Australia/Brisbane"), + ("Australia/Hobart", "(GMT+1000) Australia/Hobart"), + ("Australia/Lindeman", "(GMT+1000) Australia/Lindeman"), + ("Australia/Melbourne", "(GMT+1000) Australia/Melbourne"), + ("Australia/Sydney", "(GMT+1000) Australia/Sydney"), + ("Pacific/Chuuk", "(GMT+1000) Pacific/Chuuk"), + ("Pacific/Guam", "(GMT+1000) Pacific/Guam"), + ( + "Pacific/Port_Moresby", + "(GMT+1000) Pacific/Port_Moresby", + ), + ("Pacific/Saipan", "(GMT+1000) Pacific/Saipan"), + ("Australia/Lord_Howe", "(GMT+1030) Australia/Lord_Howe"), + ("Antarctica/Casey", "(GMT+1100) Antarctica/Casey"), + ("Asia/Magadan", "(GMT+1100) Asia/Magadan"), + ("Asia/Sakhalin", "(GMT+1100) Asia/Sakhalin"), + ("Asia/Srednekolymsk", "(GMT+1100) Asia/Srednekolymsk"), + ( + "Pacific/Bougainville", + "(GMT+1100) Pacific/Bougainville", + ), + ("Pacific/Efate", "(GMT+1100) Pacific/Efate"), + ("Pacific/Guadalcanal", "(GMT+1100) Pacific/Guadalcanal"), + ("Pacific/Kosrae", "(GMT+1100) Pacific/Kosrae"), + ("Pacific/Norfolk", "(GMT+1100) Pacific/Norfolk"), + ("Pacific/Noumea", "(GMT+1100) Pacific/Noumea"), + ("Pacific/Pohnpei", "(GMT+1100) Pacific/Pohnpei"), + ("Antarctica/McMurdo", "(GMT+1200) Antarctica/McMurdo"), + ("Asia/Anadyr", "(GMT+1200) Asia/Anadyr"), + ("Asia/Kamchatka", "(GMT+1200) Asia/Kamchatka"), + ("Pacific/Auckland", "(GMT+1200) Pacific/Auckland"), + ("Pacific/Fiji", "(GMT+1200) Pacific/Fiji"), + ("Pacific/Funafuti", "(GMT+1200) Pacific/Funafuti"), + ("Pacific/Kwajalein", "(GMT+1200) Pacific/Kwajalein"), + ("Pacific/Majuro", "(GMT+1200) Pacific/Majuro"), + ("Pacific/Nauru", "(GMT+1200) Pacific/Nauru"), + ("Pacific/Tarawa", "(GMT+1200) Pacific/Tarawa"), + ("Pacific/Wake", "(GMT+1200) Pacific/Wake"), + ("Pacific/Wallis", "(GMT+1200) Pacific/Wallis"), + ("Pacific/Chatham", "(GMT+1245) Pacific/Chatham"), + ("Pacific/Apia", "(GMT+1300) Pacific/Apia"), + ("Pacific/Fakaofo", "(GMT+1300) Pacific/Fakaofo"), + ("Pacific/Kanton", "(GMT+1300) Pacific/Kanton"), + ("Pacific/Tongatapu", "(GMT+1300) Pacific/Tongatapu"), + ("Pacific/Kiritimati", "(GMT+1400) Pacific/Kiritimati"), + ], + default="UTC", + max_length=255, + ), + ), + ] From 391f0cc3350e6d64250cf825cc744c7b2c60c5a9 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 7 Apr 2025 13:55:42 -0400 Subject: [PATCH 12/14] [tests] Trying to parallel tests --- .drone.yml | 2 +- poetry.lock | 75 ++++++++++++++++++++++++++++++++++++++++++++++---- pyproject.toml | 1 + 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/.drone.yml b/.drone.yml index 80aedc2..c1d66b1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -16,7 +16,7 @@ steps: - pip install poetry - poetry install --with test # Start with a fresh database (which is already running as a service from Drone) - - poetry run pytest --cov-report term:skip-covered --cov=vrobbler tests + - poetry run pytest -n 5 --cov-report term:skip-covered --cov=vrobbler tests environment: VROBBLER_DATABASE_URL: sqlite:///test.db volumes: diff --git a/poetry.lock b/poetry.lock index 0b4de32..3406318 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -1345,6 +1345,21 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "execnet" +version = "2.1.1" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, + {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "executing" version = "2.2.0" @@ -1746,7 +1761,7 @@ description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version < \"3.10\"" +markers = "python_version == \"3.9\"" files = [ {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, @@ -1771,7 +1786,7 @@ description = "Read resources from Python packages" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version < \"3.10\"" +markers = "python_version == \"3.9\"" files = [ {file = "importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec"}, {file = "importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c"}, @@ -1895,6 +1910,7 @@ python-versions = "*" groups = ["main"] files = [ {file = "jusText-3.0.1-py2.py3-none-any.whl", hash = "sha256:e0fb882dd7285415709f4b7466aed23d6b98b7b89404c36e8a2e730facfed02b"}, + {file = "justext-3.0.1-py2.py3-none-any.whl", hash = "sha256:0a5225c5cd7c5a124fec7bfa9a55110a73135e8b58ce784470af67d051ac9fd3"}, {file = "justext-3.0.1.tar.gz", hash = "sha256:b6ed2fb6c5d21618e2e34b2295c4edfc0bcece3bd549ed5c8ef5a8d20f0b3451"}, ] @@ -2905,6 +2921,18 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["test"] +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + [[package]] name = "pycodestyle" version = "2.12.1" @@ -3155,6 +3183,22 @@ enabler = ["pytest-enabler (>=2.2)"] test = ["pytest (>=6,!=8.1.*)"] type = ["pytest-mypy"] +[[package]] +name = "pytest-forked" +version = "1.6.0" +description = "run tests in isolated forked subprocesses" +optional = false +python-versions = ">=3.7" +groups = ["test"] +files = [ + {file = "pytest-forked-1.6.0.tar.gz", hash = "sha256:4dafd46a9a600f65d822b8f605133ecf5b3e1941ebb3588e943b4e3eb71a5a3f"}, + {file = "pytest_forked-1.6.0-py3-none-any.whl", hash = "sha256:810958f66a91afb1a1e2ae83089d8dc1cd2437ac96b12963042fbb9fb4d16af0"}, +] + +[package.dependencies] +py = "*" +pytest = ">=3.10" + [[package]] name = "pytest-isort" version = "3.1.0" @@ -3187,6 +3231,27 @@ files = [ docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] testing = ["pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-virtualenv", "types-setuptools"] +[[package]] +name = "pytest-xdist" +version = "1.34.0" +description = "pytest xdist plugin for distributed testing and loop-on-failing modes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["test"] +files = [ + {file = "pytest-xdist-1.34.0.tar.gz", hash = "sha256:340e8e83e2a4c0d861bdd8d05c5d7b7143f6eea0aba902997db15c2a86be04ee"}, + {file = "pytest_xdist-1.34.0-py2.py3-none-any.whl", hash = "sha256:ba5d10729372d65df3ac150872f9df5d2ed004a3b0d499cc0164aafedd8c7b66"}, +] + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=4.4.0" +pytest-forked = "*" +six = "*" + +[package.extras] +testing = ["filelock"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -4423,7 +4488,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version < \"3.10\"" +markers = "python_version == \"3.9\"" files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, @@ -4440,4 +4505,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "887ed21d459eee37807de2edf318cf7fe27de718ff8543c9ed660d3b344aa516" +content-hash = "9721dbf7d63211831a91880d88f5bfbacd4cc8ea462ca6a76ec3679699521bdf" diff --git a/pyproject.toml b/pyproject.toml index 9347732..4e621ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ pytest = "^7.1" pytest-black = "^0.3.12" pytest-cov = "^3.0" pytest-django = "^4.5.2" +pytest-xdist= "^1.0.0" pytest-flake8 = "^1.1" pytest-isort = "^3.0" pytest-runner = "^6.0" From f45541de6d2497476e03cb21e3bb7ce72303be84 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 7 Apr 2025 14:26:54 -0400 Subject: [PATCH 13/14] [deps] Fixes premature bump to py312 Turns out our deployment uses python 3.11, so we need to roll back the pillow and pendulum upgrades so they work with python 3.11 ... how irritating to have all this version bump nonesense. --- poetry.lock | 283 +++++++++++++++++++------------------------------ pyproject.toml | 6 +- 2 files changed, 113 insertions(+), 176 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3406318..79c3523 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1516,7 +1516,7 @@ description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" +markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\"" files = [ {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, @@ -1910,7 +1910,6 @@ python-versions = "*" groups = ["main"] files = [ {file = "jusText-3.0.1-py2.py3-none-any.whl", hash = "sha256:e0fb882dd7285415709f4b7466aed23d6b98b7b89404c36e8a2e730facfed02b"}, - {file = "justext-3.0.1-py2.py3-none-any.whl", hash = "sha256:0a5225c5cd7c5a124fec7bfa9a55110a73135e8b58ce784470af67d051ac9fd3"}, {file = "justext-3.0.1.tar.gz", hash = "sha256:b6ed2fb6c5d21618e2e34b2295c4edfc0bcece3bd549ed5c8ef5a8d20f0b3451"}, ] @@ -2515,103 +2514,38 @@ setuptools = "*" [[package]] name = "pendulum" -version = "3.0.0" +version = "2.1.2" description = "Python datetimes made easy" optional = false -python-versions = ">=3.8" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" groups = ["main"] files = [ - {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, - {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, - {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, - {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, - {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, - {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, - {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"}, - {file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, - {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, - {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, - {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, + {file = "pendulum-2.1.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:b6c352f4bd32dff1ea7066bd31ad0f71f8d8100b9ff709fb343f3b86cee43efe"}, + {file = "pendulum-2.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:318f72f62e8e23cd6660dbafe1e346950281a9aed144b5c596b2ddabc1d19739"}, + {file = "pendulum-2.1.2-cp35-cp35m-macosx_10_15_x86_64.whl", hash = "sha256:0731f0c661a3cb779d398803655494893c9f581f6488048b3fb629c2342b5394"}, + {file = "pendulum-2.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3481fad1dc3f6f6738bd575a951d3c15d4b4ce7c82dce37cf8ac1483fde6e8b0"}, + {file = "pendulum-2.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9702069c694306297ed362ce7e3c1ef8404ac8ede39f9b28b7c1a7ad8c3959e3"}, + {file = "pendulum-2.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:fb53ffa0085002ddd43b6ca61a7b34f2d4d7c3ed66f931fe599e1a531b42af9b"}, + {file = "pendulum-2.1.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:c501749fdd3d6f9e726086bf0cd4437281ed47e7bca132ddb522f86a1645d360"}, + {file = "pendulum-2.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c807a578a532eeb226150d5006f156632df2cc8c5693d778324b43ff8c515dd0"}, + {file = "pendulum-2.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2d1619a721df661e506eff8db8614016f0720ac171fe80dda1333ee44e684087"}, + {file = "pendulum-2.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f888f2d2909a414680a29ae74d0592758f2b9fcdee3549887779cd4055e975db"}, + {file = "pendulum-2.1.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e95d329384717c7bf627bf27e204bc3b15c8238fa8d9d9781d93712776c14002"}, + {file = "pendulum-2.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4c9c689747f39d0d02a9f94fcee737b34a5773803a64a5fdb046ee9cac7442c5"}, + {file = "pendulum-2.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1245cd0075a3c6d889f581f6325dd8404aca5884dea7223a5566c38aab94642b"}, + {file = "pendulum-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:db0a40d8bcd27b4fb46676e8eb3c732c67a5a5e6bfab8927028224fbced0b40b"}, + {file = "pendulum-2.1.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f5e236e7730cab1644e1b87aca3d2ff3e375a608542e90fe25685dae46310116"}, + {file = "pendulum-2.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:de42ea3e2943171a9e95141f2eecf972480636e8e484ccffaf1e833929e9e052"}, + {file = "pendulum-2.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7c5ec650cb4bec4c63a89a0242cc8c3cebcec92fcfe937c417ba18277d8560be"}, + {file = "pendulum-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:33fb61601083f3eb1d15edeb45274f73c63b3c44a8524703dc143f4212bf3269"}, + {file = "pendulum-2.1.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:29c40a6f2942376185728c9a0347d7c0f07905638c83007e1d262781f1e6953a"}, + {file = "pendulum-2.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:94b1fc947bfe38579b28e1cccb36f7e28a15e841f30384b5ad6c5e31055c85d7"}, + {file = "pendulum-2.1.2.tar.gz", hash = "sha256:b06a0ca1bfe41c990bbf0c029f0b6501a7f2ec4e38bfec730712015e8860f207"}, ] [package.dependencies] -python-dateutil = ">=2.6" -tzdata = ">=2020.1" - -[package.extras] -test = ["time-machine (>=2.6.0) ; implementation_name != \"pypy\""] +python-dateutil = ">=2.6,<3.0" +pytzdata = ">=2020.1" [[package]] name = "pexpect" @@ -2646,92 +2580,83 @@ Pillow = ">=7.0" [[package]] name = "pillow" -version = "11.1.0" +version = "9.5.0" description = "Python Imaging Library (Fork)" optional = false -python-versions = ">=3.9" +python-versions = ">=3.7" groups = ["main"] files = [ - {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, - {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, - {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, - {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, - {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, - {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, - {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, - {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, - {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, - {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, - {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, - {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, - {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, - {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, - {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, - {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, - {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, - {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, - {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, - {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, - {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, - {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, - {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, - {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, - {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, - {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, - {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, - {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, - {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, - {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, - {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, - {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, - {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, - {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, - {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, - {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, - {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, - {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, - {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, - {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, - {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, - {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, - {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, - {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, - {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, - {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, - {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, - {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, - {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, - {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, - {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, - {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, - {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, - {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, - {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, - {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, - {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, - {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, - {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, - {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, - {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, - {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, - {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, - {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, - {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, + {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, + {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, + {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, + {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, + {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, + {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, + {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, + {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, + {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, + {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, + {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, + {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, + {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, + {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, + {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] -fpx = ["olefile"] -mic = ["olefile"] -tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions ; python_version < \"3.10\""] -xmp = ["defusedxml"] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] [[package]] name = "platformdirs" @@ -3325,6 +3250,18 @@ files = [ {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, ] +[[package]] +name = "pytzdata" +version = "2020.1" +description = "The Olson timezone database for Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"}, + {file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"}, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -4504,5 +4441,5 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" -python-versions = ">=3.9,<4.0" -content-hash = "9721dbf7d63211831a91880d88f5bfbacd4cc8ea462ca6a76ec3679699521bdf" +python-versions = ">=3.9,<3.12" +content-hash = "7f26416c6bd7b8fc7ab08ef771ef2aee06c3fbc8de4b5c75ec5d9494d7a85245" diff --git a/pyproject.toml b/pyproject.toml index 4e621ce..c7f4d59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "" authors = ["Colin Powell "] [tool.poetry.dependencies] -python = ">=3.9,<4.0" +python = ">=3.9,<3.12" Django = "^4.0.3" django-extensions = "^3.1.5" python-dateutil = "^2.8.2" @@ -16,7 +16,7 @@ httpx = "<=0.27.2" djangorestframework = "^3.13.1" Markdown = "^3.3.6" django-filter = "^21.1" -Pillow = "^11.1.0" +Pillow = "^9.0.1" psycopg2 = "^2.9.3" dj-database-url = "^0.5.0" django-mathfilters = "^1.0.0" @@ -41,7 +41,7 @@ beautifulsoup4 = "^4.11.2" django-storages = "^1.13.2" stream-sqlite = "^0.0.41" ipython = "^8.14.0" -pendulum = "^3.0.0" +pendulum = "^2.1.2" trafilatura = "^1.6.3" django-imagekit = "^5.0.0" thefuzz = "^0.22.1" From e65a2d300d80fa2b32b02078f14a369a06342a5f Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 7 Apr 2025 14:38:46 -0400 Subject: [PATCH 14/14] [ci] Fixes reference to wrong ssh key --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index c1d66b1..0eba99d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -30,7 +30,7 @@ steps: - vrobbler.service username: root ssh_key: - from_secret: jail_key + from_secret: unbl_ink_key command_timeout: 2m script: - pip uninstall -y vrobbler