From f821dcbbc2a6cb56c238a50362f9e9b5728012bd Mon Sep 17 00:00:00 2001 From: petitminion Date: Tue, 12 Sep 2023 16:09:34 +0000 Subject: [PATCH] Create a testing environment in production for ListenBrainz recommendation engine (troi-recommendation-playground) --- .env.dev | 1 + .gitlab-ci.yml | 4 +- api/config/settings/common.py | 7 + api/config/settings/local.py | 2 + api/config/settings/testing.py | 1 + .../commands/generate_typesense_index.py | 13 + .../radios/lb_recommendations.py | 135 +++++++ api/funkwhale_api/radios/radios.py | 61 ++- api/funkwhale_api/typesense/__init__.py | 0 api/funkwhale_api/typesense/factories.py | 111 ++++++ api/funkwhale_api/typesense/tasks.py | 108 ++++++ api/funkwhale_api/typesense/utils.py | 92 +++++ api/poetry.lock | 354 +++++++++++++++++- api/pyproject.toml | 3 + api/tests/radios/test_lb_recommendations.py | 116 ++++++ api/tests/radios/test_radios.py | 25 ++ api/tests/typesense/test_tasks.py | 58 +++ api/tests/typesense/test_utils.py | 43 +++ changes/changelog.d/1861.newfeature | 1 + 19 files changed, 1124 insertions(+), 11 deletions(-) create mode 100644 api/funkwhale_api/music/management/commands/generate_typesense_index.py create mode 100644 api/funkwhale_api/radios/lb_recommendations.py create mode 100644 api/funkwhale_api/typesense/__init__.py create mode 100644 api/funkwhale_api/typesense/factories.py create mode 100644 api/funkwhale_api/typesense/tasks.py create mode 100644 api/funkwhale_api/typesense/utils.py create mode 100644 api/tests/radios/test_lb_recommendations.py create mode 100644 api/tests/typesense/test_tasks.py create mode 100644 api/tests/typesense/test_utils.py create mode 100644 changes/changelog.d/1861.newfeature diff --git a/.env.dev b/.env.dev index 7ec5baeef..d041167ba 100644 --- a/.env.dev +++ b/.env.dev @@ -20,3 +20,4 @@ MEDIA_ROOT=/data/media # Customize to your needs POSTGRES_VERSION=11 DEBUG=true +TYPESENSE_API_KEY="apikey" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e08382be6..d836e57b4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -253,7 +253,7 @@ test_api: CACHE_URL: "redis://redis:6379/0" before_script: - cd api - - poetry install + - poetry install --all-extras script: - > poetry run pytest @@ -354,7 +354,7 @@ build_api_schema: API_TYPE: "v1" before_script: - cd api - - poetry install + - poetry install --all-extras - poetry run funkwhale-manage migrate script: - poetry run funkwhale-manage spectacular --file ../docs/schema.yml diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 290cc5337..de4117edc 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -272,6 +272,7 @@ LOCAL_APPS = ( "funkwhale_api.playlists", "funkwhale_api.subsonic", "funkwhale_api.tags", + "funkwhale_api.typesense", ) # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps @@ -934,6 +935,11 @@ CELERY_BEAT_SCHEDULE = { ), "options": {"expires": 60 * 60}, }, + "typesense.build_canonical_index": { + "task": "typesense.build_canonical_index", + "schedule": crontab(day_of_week="*/2", minute="0", hour="3"), + "options": {"expires": 60 * 60 * 24}, + }, } if env.bool("ADD_ALBUM_TAGS_FROM_TRACKS", default=True): @@ -1477,3 +1483,4 @@ TYPESENSE_HOST = env( Typesense hostname. Defaults to `localhost` on non-Docker deployments and to `typesense` on Docker deployments. """ +TYPESENSE_NUM_TYPO = env("TYPESENSE_NUM_TYPO", default=5) diff --git a/api/config/settings/local.py b/api/config/settings/local.py index cb9538cf5..b1dc93f19 100644 --- a/api/config/settings/local.py +++ b/api/config/settings/local.py @@ -149,3 +149,5 @@ MIDDLEWARE = ( "funkwhale_api.common.middleware.ProfilerMiddleware", "funkwhale_api.common.middleware.PymallocMiddleware", ) + MIDDLEWARE + +TYPESENSE_API_KEY = "apikey" diff --git a/api/config/settings/testing.py b/api/config/settings/testing.py index 17872da62..ab763b84d 100644 --- a/api/config/settings/testing.py +++ b/api/config/settings/testing.py @@ -6,3 +6,4 @@ from .common import * # noqa DEBUG = True SECRET_KEY = "a_super_secret_key!" +TYPESENSE_API_KEY = "apikey" diff --git a/api/funkwhale_api/music/management/commands/generate_typesense_index.py b/api/funkwhale_api/music/management/commands/generate_typesense_index.py new file mode 100644 index 000000000..f2280f49f --- /dev/null +++ b/api/funkwhale_api/music/management/commands/generate_typesense_index.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand + +from funkwhale_api.typesense import tasks + + +class Command(BaseCommand): + help = """ + Trigger the generation of a new typesense index for canonical Funkwhale tracks metadata. + This is use to resolve Funkwhale tracks to MusicBrainz ids""" + + def handle(self, *args, **kwargs): + tasks.build_canonical_index.delay() + self.stdout.write("Tasks launched in celery worker.") diff --git a/api/funkwhale_api/radios/lb_recommendations.py b/api/funkwhale_api/radios/lb_recommendations.py new file mode 100644 index 000000000..a50441418 --- /dev/null +++ b/api/funkwhale_api/radios/lb_recommendations.py @@ -0,0 +1,135 @@ +import logging +import time + +import troi +import troi.core +from django.core.cache import cache +from django.core.exceptions import ValidationError +from django.db.models import Q +from requests.exceptions import ConnectTimeout + +from funkwhale_api.music import models as music_models +from funkwhale_api.typesense import utils + +logger = logging.getLogger(__name__) + + +patches = troi.utils.discover_patches() + +SUPPORTED_PATCHES = patches.keys() + + +def run(config, **kwargs): + """Validate the received config and run the queryset generation""" + candidates = kwargs.pop("candidates", music_models.Track.objects.all()) + validate(config) + return TroiPatch().get_queryset(config, candidates) + + +def validate(config): + patch = config.get("patch") + if patch not in SUPPORTED_PATCHES: + raise ValidationError( + 'Invalid patch "{}". Supported patches: {}'.format( + config["patch"], SUPPORTED_PATCHES + ) + ) + + return True + + +def build_radio_queryset(patch, config, radio_qs): + """Take a troi patch and its arg, match the missing mbid and then build a radio queryset""" + + logger.info("Config used for troi radio generation is " + str(config)) + + start_time = time.time() + try: + recommendations = troi.core.generate_playlist(patch, config) + except ConnectTimeout: + raise ValueError( + "Timed out while connecting to ListenBrainz. No candidates could be retrieved for the radio." + ) + end_time_rec = time.time() + logger.info("Troi fetch took :" + str(end_time_rec - start_time)) + + if not recommendations: + raise ValueError("No candidates found by troi") + + recommended_recording_mbids = [ + recommended_recording.mbid + for recommended_recording in recommendations.playlists[0].recordings + ] + + logger.info("Searching for MusicBrainz ID in Funkwhale database") + + qs_mbid = music_models.Track.objects.all().filter( + mbid__in=recommended_recording_mbids + ) + mbids_found = [str(i.mbid) for i in qs_mbid] + + recommended_recording_mbids_not_found = [ + mbid for mbid in recommended_recording_mbids if mbid not in mbids_found + ] + cached_mbid_match = cache.get_many(recommended_recording_mbids_not_found) + + if qs_mbid and cached_mbid_match: + logger.info("MusicBrainz IDs found in Funkwhale database and redis") + mbids_found = [str(i.mbid) for i in qs_mbid] + mbids_found.extend([i for i in cached_mbid_match.keys()]) + elif qs_mbid and not cached_mbid_match: + logger.info("MusicBrainz IDs found in Funkwhale database") + mbids_found = mbids_found + elif not qs_mbid and cached_mbid_match: + logger.info("MusicBrainz IDs found in redis cache") + mbids_found = [i for i in cached_mbid_match.keys()] + else: + logger.info( + "Couldn't find any matches in Funkwhale database. Trying to match all" + ) + mbids_found = [] + + recommended_recordings_not_found = [ + i for i in recommendations.playlists[0].recordings if i.mbid not in mbids_found + ] + + logger.info("Matching missing MusicBrainz ID to Funkwhale track") + + start_time_resolv = time.time() + utils.resolve_recordings_to_fw_track(recommended_recordings_not_found) + end_time_resolv = time.time() + + logger.info( + "Resolving " + + str(len(recommended_recordings_not_found)) + + " tracks in " + + str(end_time_resolv - start_time_resolv) + ) + + cached_mbid_match = cache.get_many(recommended_recording_mbids_not_found) + + if not qs_mbid and not cached_mbid_match: + raise ValueError("No candidates found for troi radio") + + logger.info("Radio generation with troi took " + str(end_time_resolv - start_time)) + logger.info("qs_mbid is " + str(mbids_found)) + + if qs_mbid and cached_mbid_match: + return radio_qs.filter( + Q(mbid__in=mbids_found) | Q(pk__in=cached_mbid_match.values()) + ) + if qs_mbid and not cached_mbid_match: + return radio_qs.filter(mbid__in=mbids_found) + + if not qs_mbid and cached_mbid_match: + return radio_qs.filter(pk__in=cached_mbid_match.values()) + + +class TroiPatch: + code = "troi-patch" + label = "Troi Patch" + + def get_queryset(self, config, qs): + patch_string = config.pop("patch") + patch = patches[patch_string] + return build_radio_queryset(patch(), config, qs) diff --git a/api/funkwhale_api/radios/radios.py b/api/funkwhale_api/radios/radios.py index 3706cfc9f..821a181cf 100644 --- a/api/funkwhale_api/radios/radios.py +++ b/api/funkwhale_api/radios/radios.py @@ -1,4 +1,5 @@ import datetime +import json import logging import random from typing import List, Optional, Tuple @@ -12,6 +13,7 @@ from funkwhale_api.federation import fields as federation_fields from funkwhale_api.federation import models as federation_models from funkwhale_api.moderation import filters as moderation_filters from funkwhale_api.music.models import Artist, Library, Track, Upload +from funkwhale_api.radios import lb_recommendations from funkwhale_api.tags.models import Tag from . import filters, models @@ -189,9 +191,7 @@ class CustomMultiple(SessionRadio): def validate_session(self, data, **context): data = super().validate_session(data, **context) - try: - data["config"] is not None - except KeyError: + if data.get("config") is None: raise serializers.ValidationError( "You must provide a configuration for this radio" ) @@ -405,3 +405,58 @@ class RecentlyAdded(SessionRadio): Q(artist__content_category="music"), Q(creation_date__gt=date), ) + + +# Use this to experiment on the custom multiple radio with troi +@registry.register(name="troi") +class Troi(SessionRadio): + """ + Receive a vuejs generated config and use it to launch a troi radio session. + The config data should follow : + {"patch": "troi_patch_name", "troi_arg1":"troi_arg_1", "troi_arg2": ...} + Validation of the config (args) is done by troi during track fetch. + Funkwhale only checks if the patch is implemented + """ + + config = serializers.JSONField(required=True) + + def append_lb_config(self, data): + if self.session.user.settings is None: + logger.warning( + "No lb_user_name set in user settings. Some troi patches will fail" + ) + return data + elif self.session.user.settings.get("lb_user_name") is None: + logger.warning( + "No lb_user_name set in user settings. Some troi patches will fail" + ) + else: + data["user_name"] = self.session.user.settings["lb_user_name"] + + if self.session.user.settings.get("lb_user_token") is None: + logger.warning( + "No lb_user_token set in user settings. Some troi patch will fail" + ) + else: + data["user_token"] = self.session.user.settings["lb_user_token"] + + return data + + def get_queryset_kwargs(self): + kwargs = super().get_queryset_kwargs() + kwargs["config"] = self.session.config + return kwargs + + def validate_session(self, data, **context): + data = super().validate_session(data, **context) + if data.get("config") is None: + raise serializers.ValidationError( + "You must provide a configuration for this radio" + ) + return data + + def get_queryset(self, **kwargs): + qs = super().get_queryset(**kwargs) + config = self.append_lb_config(json.loads(kwargs["config"])) + + return lb_recommendations.run(config, candidates=qs) diff --git a/api/funkwhale_api/typesense/__init__.py b/api/funkwhale_api/typesense/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/funkwhale_api/typesense/factories.py b/api/funkwhale_api/typesense/factories.py new file mode 100644 index 000000000..006298f50 --- /dev/null +++ b/api/funkwhale_api/typesense/factories.py @@ -0,0 +1,111 @@ +from troi import Artist, Element, Playlist, Recording +from troi.patch import Patch + +recording_list = [ + Recording( + name="I Want It That Way", + mbid="87dfa566-21c3-45ed-bc42-1d345b8563fa", + artist=Artist(name="artist_name"), + ), + Recording(name="Untouchable", artist=Artist(name="Another lol")), + Recording( + name="The Perfect Kiss", + mbid="ec0da94e-fbfe-4eb0-968e-024d4c32d1d0", + artist=Artist(name="artist_name2"), + ), + Recording( + name="Love Your Voice", + mbid="93726547-f8c0-4efd-8e16-d2dee76500f6", + artist=Artist(name="artist_name"), + ), + Recording( + name="Hall of Fame", + mbid="395bd5a1-79cc-4e04-8869-ca9eabc78d09", + artist=Artist(name="artist_name_3"), + ), +] + + +class DummyElement(Element): + """Dummy element that returns a fixed playlist for testing""" + + @staticmethod + def outputs(): + return [Playlist] + + def read(self, sources): + recordings = recording_list + + return [ + Playlist( + name="Test Export Playlist", + description="A playlist to test exporting playlists to spotify", + recordings=recordings, + ) + ] + + +class DummyPatch(Patch): + """Dummy patch that always returns a fixed set of recordings for testing""" + + @staticmethod + def slug(): + return "test-patch" + + def create(self, inputs): + return DummyElement() + + @staticmethod + def outputs(): + return [Recording] + + +recommended_recording_mbids = [ + "87dfa566-21c3-45ed-bc42-1d345b8563fa", + "ec0da94e-fbfe-4eb0-968e-024d4c32d1d0", + "93726547-f8c0-4efd-8e16-d2dee76500f6", + "395bd5a1-79cc-4e04-8869-ca9eabc78d09", +] + +typesense_search_result = { + "facet_counts": [], + "found": 1, + "out_of": 1, + "page": 1, + "request_params": { + "collection_name": "canonical_fw_data", + "per_page": 10, + "q": "artist_nameiwantitthatway", + }, + "search_time_ms": 1, + "hits": [ + { + "highlights": [ + { + "field": "combined", + "snippet": "string", + "matched_tokens": ["string"], + } + ], + "document": { + "pk": "1", + "combined": "artist_nameiwantitthatway", + }, + "text_match": 130916, + }, + { + "highlights": [ + { + "field": "combined", + "snippet": "string", + "matched_tokens": ["string"], + } + ], + "document": { + "pk": "2", + "combined": "artist_nameiwantitthatway", + }, + "text_match": 130916, + }, + ], +} diff --git a/api/funkwhale_api/typesense/tasks.py b/api/funkwhale_api/typesense/tasks.py new file mode 100644 index 000000000..febb96eea --- /dev/null +++ b/api/funkwhale_api/typesense/tasks.py @@ -0,0 +1,108 @@ +import logging + +from django.conf import settings + +from funkwhale_api.music import models as music_models +from funkwhale_api.taskapp import celery + +from . import utils + +logger = logging.getLogger(__name__) + + +class TypesenseNotActivate(Exception): + pass + + +if not settings.TYPESENSE_API_KEY: + logger.info( + "Typesense is not activated. You can enable it by setting the TYPESENSE_API_KEY env variable." + ) +else: + import typesense + from typesense.exceptions import ObjectAlreadyExists + + +api_key = settings.TYPESENSE_API_KEY +host = settings.TYPESENSE_HOST +port = settings.TYPESENSE_PORT +protocol = settings.TYPESENSE_PROTOCOL + +collection_name = "canonical_fw_data" +BATCH_SIZE = 10000 + + +@celery.app.task(name="typesense.add_tracks_to_index") +def add_tracks_to_index(tracks_pk): + """ + This will add fw tracks data to the typesense index. It will concatenate the artist name + and the track title into one string. + """ + + client = typesense.Client( + { + "api_key": api_key, + "nodes": [{"host": host, "port": port, "protocol": protocol}], + "connection_timeout_seconds": 2, + } + ) + + try: + logger.info(f"Updating index {collection_name}") + tracks = music_models.Track.objects.all().filter(pk__in=tracks_pk) + documents = [] + for track in tracks: + document = dict() + document["pk"] = track.pk + document["combined"] = utils.delete_non_alnum_characters( + track.artist.name + track.title + ) + documents.append(document) + + client.collections[collection_name].documents.import_( + documents, {"action": "upsert"} + ) + + except typesense.exceptions.TypesenseClientError as err: + logger.error(f"Can't build index: {str(err)}") + + +@celery.app.task(name="typesense.build_canonical_index") +def build_canonical_index(): + if not settings.TYPESENSE_API_KEY: + raise TypesenseNotActivate( + "Typesense is not activated. You can enable it by setting the TYPESENSE_API_KEY env variable." + ) + + schema = { + "name": collection_name, + "fields": [ + {"name": "combined", "type": "string"}, + {"name": "pk", "type": "int32"}, + ], + "default_sorting_field": "pk", + } + client = typesense.Client( + { + "api_key": api_key, + "nodes": [{"host": host, "port": port, "protocol": protocol}], + "connection_timeout_seconds": 2, + } + ) + try: + client.collections.create(schema) + except ObjectAlreadyExists: + pass + + tracks = music_models.Track.objects.all().values_list("pk", flat=True) + total_tracks = tracks.count() + total_batches = (total_tracks - 1) // BATCH_SIZE + 1 + + for i in range(total_batches): + start_index = i * BATCH_SIZE + end_index = (i + 1) * (BATCH_SIZE - 1) + batch_tracks = tracks[start_index:end_index] + logger.info( + f"Launching async task to add {str(batch_tracks)} tracks pks to index" + ) + add_tracks_to_index.delay(list(batch_tracks)) diff --git a/api/funkwhale_api/typesense/utils.py b/api/funkwhale_api/typesense/utils.py new file mode 100644 index 000000000..4e2d4b70d --- /dev/null +++ b/api/funkwhale_api/typesense/utils.py @@ -0,0 +1,92 @@ +import logging +import re + +import unidecode +from django.conf import settings +from django.core.cache import cache +from lb_matching_tools.cleaner import MetadataCleaner + +from funkwhale_api.music import models as music_models + +logger = logging.getLogger(__name__) + +api_key = settings.TYPESENSE_API_KEY +host = settings.TYPESENSE_HOST +port = settings.TYPESENSE_PORT +protocol = settings.TYPESENSE_PROTOCOL +TYPESENSE_NUM_TYPO = settings.TYPESENSE_NUM_TYPO + + +class TypesenseNotActivate(Exception): + pass + + +if not settings.TYPESENSE_API_KEY: + logger.info( + "Typesense is not activated. You can enable it by setting the TYPESENSE_API_KEY env variable." + ) +else: + import typesense + + +def delete_non_alnum_characters(text): + return unidecode.unidecode(re.sub(r"[^\w]+", "", text).lower()) + + +def resolve_recordings_to_fw_track(recordings): + """ + Tries to match a troi recording entity to a fw track using the typesense index. + It will save the results in the match_mbid attribute of the Track table. + For test purposes : if multiple fw tracks are returned, we log the information + but only keep the best result in db to avoid duplicates. + """ + + if not settings.TYPESENSE_API_KEY: + raise TypesenseNotActivate( + "Typesense is not activated. You can enable it by setting the TYPESENSE_API_KEY env variable." + ) + + client = typesense.Client( + { + "api_key": api_key, + "nodes": [{"host": host, "port": port, "protocol": protocol}], + "connection_timeout_seconds": 2, + } + ) + + mc = MetadataCleaner() + + for recording in recordings: + rec = mc.clean_recording(recording.name) + artist = mc.clean_artist(recording.artist.name) + canonical_name_for_track = delete_non_alnum_characters(artist + rec) + + logger.debug(f"Trying to resolve : {canonical_name_for_track}") + + search_parameters = { + "q": canonical_name_for_track, + "query_by": "combined", + "num_typos": TYPESENSE_NUM_TYPO, + "drop_tokens_threshold": 0, + } + matches = client.collections["canonical_fw_data"].documents.search( + search_parameters + ) + + if matches["hits"]: + hit = matches["hits"][0] + pk = hit["document"]["pk"] + logger.debug(f"Saving match for track with primary key {pk}") + cache.set(recording.mbid, pk) + + if settings.DEBUG and matches["hits"][1]: + for hit in matches["hits"][1:]: + pk = hit["document"]["pk"] + fw_track = music_models.Track.objects.get(pk=pk) + logger.info( + f"Duplicate match found for {fw_track.artist.name} {fw_track.title} \ + and primary key {pk}. Skipping because of better match." + ) + else: + logger.debug("No match found in fw db") + return cache.get_many([rec.mbid for rec in recordings]) diff --git a/api/poetry.lock b/api/poetry.lock index 8af3c7d02..e61ff25cd 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -836,6 +836,16 @@ files = [ {file = "constantly-15.1.0.tar.gz", hash = "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35"}, ] +[[package]] +name = "countryinfo" +version = "0.1.2" +description = "countryinfo is a python module for returning data about countries, ISO info and states/provinces within them." +optional = false +python-versions = ">3.0.0" +files = [ + {file = "countryinfo-0.1.2-py3-none-any.whl", hash = "sha256:fd518b3fd8899f6520518320ac17b67bf410c7db5044c61cb191f802bb85c34d"}, +] + [[package]] name = "coverage" version = "6.5.0" @@ -1975,6 +1985,24 @@ files = [ {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, ] +[[package]] +name = "lb-matching-tools" +version = "0.1.0" +description = "" +optional = false +python-versions = ">=3.7" +files = [] +develop = false + +[package.dependencies] +regex = "*" + +[package.source] +type = "git" +url = "https://github.com/metabrainz/listenbrainz-matching-tools.git" +reference = "main" +resolved_reference = "5de037ab3e35f3d45d2c6b2b458a818042dd4b12" + [[package]] name = "lxml" version = "4.9.3" @@ -2125,6 +2153,17 @@ files = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +[[package]] +name = "more-itertools" +version = "10.1.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.1.0.tar.gz", hash = "sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a"}, + {file = "more_itertools-10.1.0-py3-none-any.whl", hash = "sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6"}, +] + [[package]] name = "msgpack" version = "1.0.5" @@ -2562,6 +2601,75 @@ files = [ {file = "psycopg2-2.9.7.tar.gz", hash = "sha256:f00cc35bd7119f1fed17b85bd1007855194dde2cbd8de01ab8ebb17487440ad8"}, ] +[[package]] +name = "psycopg2-binary" +version = "2.9.7" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.6" +files = [ + {file = "psycopg2-binary-2.9.7.tar.gz", hash = "sha256:1b918f64a51ffe19cd2e230b3240ba481330ce1d4b7875ae67305bd1d37b041c"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ea5f8ee87f1eddc818fc04649d952c526db4426d26bab16efbe5a0c52b27d6ab"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2993ccb2b7e80844d534e55e0f12534c2871952f78e0da33c35e648bf002bbff"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbbc3c5d15ed76b0d9db7753c0db40899136ecfe97d50cbde918f630c5eb857a"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:692df8763b71d42eb8343f54091368f6f6c9cfc56dc391858cdb3c3ef1e3e584"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dcfd5d37e027ec393a303cc0a216be564b96c80ba532f3d1e0d2b5e5e4b1e6e"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17cc17a70dfb295a240db7f65b6d8153c3d81efb145d76da1e4a096e9c5c0e63"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e5666632ba2b0d9757b38fc17337d84bdf932d38563c5234f5f8c54fd01349c9"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7db7b9b701974c96a88997d458b38ccb110eba8f805d4b4f74944aac48639b42"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c82986635a16fb1fa15cd5436035c88bc65c3d5ced1cfaac7f357ee9e9deddd4"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4fe13712357d802080cfccbf8c6266a3121dc0e27e2144819029095ccf708372"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-win32.whl", hash = "sha256:122641b7fab18ef76b18860dd0c772290566b6fb30cc08e923ad73d17461dc63"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-win_amd64.whl", hash = "sha256:f8651cf1f144f9ee0fa7d1a1df61a9184ab72962531ca99f077bbdcba3947c58"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ecc15666f16f97709106d87284c136cdc82647e1c3f8392a672616aed3c7151"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fbb1184c7e9d28d67671992970718c05af5f77fc88e26fd7136613c4ece1f89"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a7968fd20bd550431837656872c19575b687f3f6f98120046228e451e4064df"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:094af2e77a1976efd4956a031028774b827029729725e136514aae3cdf49b87b"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26484e913d472ecb6b45937ea55ce29c57c662066d222fb0fbdc1fab457f18c5"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f309b77a7c716e6ed9891b9b42953c3ff7d533dc548c1e33fddc73d2f5e21f9"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6d92e139ca388ccfe8c04aacc163756e55ba4c623c6ba13d5d1595ed97523e4b"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2df562bb2e4e00ee064779902d721223cfa9f8f58e7e52318c97d139cf7f012d"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4eec5d36dbcfc076caab61a2114c12094c0b7027d57e9e4387b634e8ab36fd44"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1011eeb0c51e5b9ea1016f0f45fa23aca63966a4c0afcf0340ccabe85a9f65bd"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-win32.whl", hash = "sha256:ded8e15f7550db9e75c60b3d9fcbc7737fea258a0f10032cdb7edc26c2a671fd"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-win_amd64.whl", hash = "sha256:8a136c8aaf6615653450817a7abe0fc01e4ea720ae41dfb2823eccae4b9062a3"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2dec5a75a3a5d42b120e88e6ed3e3b37b46459202bb8e36cd67591b6e5feebc1"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc10da7e7df3380426521e8c1ed975d22df678639da2ed0ec3244c3dc2ab54c8"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee919b676da28f78f91b464fb3e12238bd7474483352a59c8a16c39dfc59f0c5"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb1c0e682138f9067a58fc3c9a9bf1c83d8e08cfbee380d858e63196466d5c86"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00d8db270afb76f48a499f7bb8fa70297e66da67288471ca873db88382850bf4"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9b0c2b466b2f4d89ccc33784c4ebb1627989bd84a39b79092e560e937a11d4ac"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:51d1b42d44f4ffb93188f9b39e6d1c82aa758fdb8d9de65e1ddfe7a7d250d7ad"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:11abdbfc6f7f7dea4a524b5f4117369b0d757725798f1593796be6ece20266cb"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f02f4a72cc3ab2565c6d9720f0343cb840fb2dc01a2e9ecb8bc58ccf95dc5c06"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-win32.whl", hash = "sha256:81d5dd2dd9ab78d31a451e357315f201d976c131ca7d43870a0e8063b6b7a1ec"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-win_amd64.whl", hash = "sha256:62cb6de84d7767164a87ca97e22e5e0a134856ebcb08f21b621c6125baf61f16"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:59f7e9109a59dfa31efa022e94a244736ae401526682de504e87bd11ce870c22"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:95a7a747bdc3b010bb6a980f053233e7610276d55f3ca506afff4ad7749ab58a"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c721ee464e45ecf609ff8c0a555018764974114f671815a0a7152aedb9f3343"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4f37bbc6588d402980ffbd1f3338c871368fb4b1cfa091debe13c68bb3852b3"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac83ab05e25354dad798401babaa6daa9577462136ba215694865394840e31f8"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:024eaeb2a08c9a65cd5f94b31ace1ee3bb3f978cd4d079406aef85169ba01f08"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1c31c2606ac500dbd26381145684d87730a2fac9a62ebcfbaa2b119f8d6c19f4"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:42a62ef0e5abb55bf6ffb050eb2b0fcd767261fa3faf943a4267539168807522"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7952807f95c8eba6a8ccb14e00bf170bb700cafcec3924d565235dffc7dc4ae8"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e02bc4f2966475a7393bd0f098e1165d470d3fa816264054359ed4f10f6914ea"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-win32.whl", hash = "sha256:fdca0511458d26cf39b827a663d7d87db6f32b93efc22442a742035728603d5f"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:d0b16e5bb0ab78583f0ed7ab16378a0f8a89a27256bb5560402749dbe8a164d7"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6822c9c63308d650db201ba22fe6648bd6786ca6d14fdaf273b17e15608d0852"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f94cb12150d57ea433e3e02aabd072205648e86f1d5a0a692d60242f7809b15"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5ee89587696d808c9a00876065d725d4ae606f5f7853b961cdbc348b0f7c9a1"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad5ec10b53cbb57e9a2e77b67e4e4368df56b54d6b00cc86398578f1c635f329"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:642df77484b2dcaf87d4237792246d8068653f9e0f5c025e2c692fc56b0dda70"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6a8b575ac45af1eaccbbcdcf710ab984fd50af048fe130672377f78aaff6fc1"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f955aa50d7d5220fcb6e38f69ea126eafecd812d96aeed5d5f3597f33fad43bb"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ad26d4eeaa0d722b25814cce97335ecf1b707630258f14ac4d2ed3d1d8415265"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ced63c054bdaf0298f62681d5dcae3afe60cbae332390bfb1acf0e23dcd25fc8"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2b04da24cbde33292ad34a40db9832a80ad12de26486ffeda883413c9e1b1d5e"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-win32.whl", hash = "sha256:18f12632ab516c47c1ac4841a78fddea6508a8284c7cf0f292cb1a523f2e2379"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb3b8d55924a6058a26db69fb1d3e7e32695ff8b491835ba9f479537e14dcf9f"}, +] + [[package]] name = "ptyprocess" version = "0.7.0" @@ -2739,6 +2847,25 @@ files = [ [package.dependencies] pylint = ">=1.7" +[[package]] +name = "pylistenbrainz" +version = "0.5.2" +description = "A simple ListenBrainz client library for Python" +optional = false +python-versions = ">=3.5" +files = [] +develop = false + +[package.dependencies] +importlib-metadata = {version = ">=3.10.0", markers = "python_version < \"3.10\""} +requests = ">=2.23.0" + +[package.source] +type = "git" +url = "https://github.com/metabrainz/pylistenbrainz.git" +reference = "v0.5.2" +resolved_reference = "f66414d2da3a260b9d4322d42f98ec7a6d6b982f" + [[package]] name = "pyopenssl" version = "23.2.0" @@ -3066,6 +3193,103 @@ files = [ attrs = ">=22.2.0" rpds-py = ">=0.7.0" +[[package]] +name = "regex" +version = "2023.8.8" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.6" +files = [ + {file = "regex-2023.8.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88900f521c645f784260a8d346e12a1590f79e96403971241e64c3a265c8ecdb"}, + {file = "regex-2023.8.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3611576aff55918af2697410ff0293d6071b7e00f4b09e005d614686ac4cd57c"}, + {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8a0ccc8f2698f120e9e5742f4b38dc944c38744d4bdfc427616f3a163dd9de5"}, + {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c662a4cbdd6280ee56f841f14620787215a171c4e2d1744c9528bed8f5816c96"}, + {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf0633e4a1b667bfe0bb10b5e53fe0d5f34a6243ea2530eb342491f1adf4f739"}, + {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:551ad543fa19e94943c5b2cebc54c73353ffff08228ee5f3376bd27b3d5b9800"}, + {file = "regex-2023.8.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54de2619f5ea58474f2ac211ceea6b615af2d7e4306220d4f3fe690c91988a61"}, + {file = "regex-2023.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ec4b3f0aebbbe2fc0134ee30a791af522a92ad9f164858805a77442d7d18570"}, + {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ae646c35cb9f820491760ac62c25b6d6b496757fda2d51be429e0e7b67ae0ab"}, + {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca339088839582d01654e6f83a637a4b8194d0960477b9769d2ff2cfa0fa36d2"}, + {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d9b6627408021452dcd0d2cdf8da0534e19d93d070bfa8b6b4176f99711e7f90"}, + {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:bd3366aceedf274f765a3a4bc95d6cd97b130d1dda524d8f25225d14123c01db"}, + {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7aed90a72fc3654fba9bc4b7f851571dcc368120432ad68b226bd593f3f6c0b7"}, + {file = "regex-2023.8.8-cp310-cp310-win32.whl", hash = "sha256:80b80b889cb767cc47f31d2b2f3dec2db8126fbcd0cff31b3925b4dc6609dcdb"}, + {file = "regex-2023.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:b82edc98d107cbc7357da7a5a695901b47d6eb0420e587256ba3ad24b80b7d0b"}, + {file = "regex-2023.8.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1e7d84d64c84ad97bf06f3c8cb5e48941f135ace28f450d86af6b6512f1c9a71"}, + {file = "regex-2023.8.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce0f9fbe7d295f9922c0424a3637b88c6c472b75eafeaff6f910494a1fa719ef"}, + {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06c57e14ac723b04458df5956cfb7e2d9caa6e9d353c0b4c7d5d54fcb1325c46"}, + {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7a9aaa5a1267125eef22cef3b63484c3241aaec6f48949b366d26c7250e0357"}, + {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b7408511fca48a82a119d78a77c2f5eb1b22fe88b0d2450ed0756d194fe7a9a"}, + {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14dc6f2d88192a67d708341f3085df6a4f5a0c7b03dec08d763ca2cd86e9f559"}, + {file = "regex-2023.8.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48c640b99213643d141550326f34f0502fedb1798adb3c9eb79650b1ecb2f177"}, + {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0085da0f6c6393428bf0d9c08d8b1874d805bb55e17cb1dfa5ddb7cfb11140bf"}, + {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:964b16dcc10c79a4a2be9f1273fcc2684a9eedb3906439720598029a797b46e6"}, + {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7ce606c14bb195b0e5108544b540e2c5faed6843367e4ab3deb5c6aa5e681208"}, + {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:40f029d73b10fac448c73d6eb33d57b34607f40116e9f6e9f0d32e9229b147d7"}, + {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3b8e6ea6be6d64104d8e9afc34c151926f8182f84e7ac290a93925c0db004bfd"}, + {file = "regex-2023.8.8-cp311-cp311-win32.whl", hash = "sha256:942f8b1f3b223638b02df7df79140646c03938d488fbfb771824f3d05fc083a8"}, + {file = "regex-2023.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:51d8ea2a3a1a8fe4f67de21b8b93757005213e8ac3917567872f2865185fa7fb"}, + {file = "regex-2023.8.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e951d1a8e9963ea51efd7f150450803e3b95db5939f994ad3d5edac2b6f6e2b4"}, + {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704f63b774218207b8ccc6c47fcef5340741e5d839d11d606f70af93ee78e4d4"}, + {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22283c769a7b01c8ac355d5be0715bf6929b6267619505e289f792b01304d898"}, + {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91129ff1bb0619bc1f4ad19485718cc623a2dc433dff95baadbf89405c7f6b57"}, + {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de35342190deb7b866ad6ba5cbcccb2d22c0487ee0cbb251efef0843d705f0d4"}, + {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b993b6f524d1e274a5062488a43e3f9f8764ee9745ccd8e8193df743dbe5ee61"}, + {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3026cbcf11d79095a32d9a13bbc572a458727bd5b1ca332df4a79faecd45281c"}, + {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:293352710172239bf579c90a9864d0df57340b6fd21272345222fb6371bf82b3"}, + {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d909b5a3fff619dc7e48b6b1bedc2f30ec43033ba7af32f936c10839e81b9217"}, + {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3d370ff652323c5307d9c8e4c62efd1956fb08051b0e9210212bc51168b4ff56"}, + {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:b076da1ed19dc37788f6a934c60adf97bd02c7eea461b73730513921a85d4235"}, + {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e9941a4ada58f6218694f382e43fdd256e97615db9da135e77359da257a7168b"}, + {file = "regex-2023.8.8-cp36-cp36m-win32.whl", hash = "sha256:a8c65c17aed7e15a0c824cdc63a6b104dfc530f6fa8cb6ac51c437af52b481c7"}, + {file = "regex-2023.8.8-cp36-cp36m-win_amd64.whl", hash = "sha256:aadf28046e77a72f30dcc1ab185639e8de7f4104b8cb5c6dfa5d8ed860e57236"}, + {file = "regex-2023.8.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:423adfa872b4908843ac3e7a30f957f5d5282944b81ca0a3b8a7ccbbfaa06103"}, + {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ae594c66f4a7e1ea67232a0846649a7c94c188d6c071ac0210c3e86a5f92109"}, + {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e51c80c168074faa793685656c38eb7a06cbad7774c8cbc3ea05552d615393d8"}, + {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:09b7f4c66aa9d1522b06e31a54f15581c37286237208df1345108fcf4e050c18"}, + {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e73e5243af12d9cd6a9d6a45a43570dbe2e5b1cdfc862f5ae2b031e44dd95a8"}, + {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:941460db8fe3bd613db52f05259c9336f5a47ccae7d7def44cc277184030a116"}, + {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f0ccf3e01afeb412a1a9993049cb160d0352dba635bbca7762b2dc722aa5742a"}, + {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2e9216e0d2cdce7dbc9be48cb3eacb962740a09b011a116fd7af8c832ab116ca"}, + {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5cd9cd7170459b9223c5e592ac036e0704bee765706445c353d96f2890e816c8"}, + {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4873ef92e03a4309b3ccd8281454801b291b689f6ad45ef8c3658b6fa761d7ac"}, + {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:239c3c2a339d3b3ddd51c2daef10874410917cd2b998f043c13e2084cb191684"}, + {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1005c60ed7037be0d9dea1f9c53cc42f836188227366370867222bda4c3c6bd7"}, + {file = "regex-2023.8.8-cp37-cp37m-win32.whl", hash = "sha256:e6bd1e9b95bc5614a7a9c9c44fde9539cba1c823b43a9f7bc11266446dd568e3"}, + {file = "regex-2023.8.8-cp37-cp37m-win_amd64.whl", hash = "sha256:9a96edd79661e93327cfeac4edec72a4046e14550a1d22aa0dd2e3ca52aec921"}, + {file = "regex-2023.8.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2181c20ef18747d5f4a7ea513e09ea03bdd50884a11ce46066bb90fe4213675"}, + {file = "regex-2023.8.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a2ad5add903eb7cdde2b7c64aaca405f3957ab34f16594d2b78d53b8b1a6a7d6"}, + {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9233ac249b354c54146e392e8a451e465dd2d967fc773690811d3a8c240ac601"}, + {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920974009fb37b20d32afcdf0227a2e707eb83fe418713f7a8b7de038b870d0b"}, + {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2b6c5dfe0929b6c23dde9624483380b170b6e34ed79054ad131b20203a1a63"}, + {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96979d753b1dc3b2169003e1854dc67bfc86edf93c01e84757927f810b8c3c93"}, + {file = "regex-2023.8.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ae54a338191e1356253e7883d9d19f8679b6143703086245fb14d1f20196be9"}, + {file = "regex-2023.8.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2162ae2eb8b079622176a81b65d486ba50b888271302190870b8cc488587d280"}, + {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c884d1a59e69e03b93cf0dfee8794c63d7de0ee8f7ffb76e5f75be8131b6400a"}, + {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf9273e96f3ee2ac89ffcb17627a78f78e7516b08f94dc435844ae72576a276e"}, + {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:83215147121e15d5f3a45d99abeed9cf1fe16869d5c233b08c56cdf75f43a504"}, + {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f7454aa427b8ab9101f3787eb178057c5250478e39b99540cfc2b889c7d0586"}, + {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0640913d2c1044d97e30d7c41728195fc37e54d190c5385eacb52115127b882"}, + {file = "regex-2023.8.8-cp38-cp38-win32.whl", hash = "sha256:0c59122ceccb905a941fb23b087b8eafc5290bf983ebcb14d2301febcbe199c7"}, + {file = "regex-2023.8.8-cp38-cp38-win_amd64.whl", hash = "sha256:c12f6f67495ea05c3d542d119d270007090bad5b843f642d418eb601ec0fa7be"}, + {file = "regex-2023.8.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:82cd0a69cd28f6cc3789cc6adeb1027f79526b1ab50b1f6062bbc3a0ccb2dbc3"}, + {file = "regex-2023.8.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bb34d1605f96a245fc39790a117ac1bac8de84ab7691637b26ab2c5efb8f228c"}, + {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:987b9ac04d0b38ef4f89fbc035e84a7efad9cdd5f1e29024f9289182c8d99e09"}, + {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dd6082f4e2aec9b6a0927202c85bc1b09dcab113f97265127c1dc20e2e32495"}, + {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7eb95fe8222932c10d4436e7a6f7c99991e3fdd9f36c949eff16a69246dee2dc"}, + {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7098c524ba9f20717a56a8d551d2ed491ea89cbf37e540759ed3b776a4f8d6eb"}, + {file = "regex-2023.8.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b694430b3f00eb02c594ff5a16db30e054c1b9589a043fe9174584c6efa8033"}, + {file = "regex-2023.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2aeab3895d778155054abea5238d0eb9a72e9242bd4b43f42fd911ef9a13470"}, + {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:988631b9d78b546e284478c2ec15c8a85960e262e247b35ca5eaf7ee22f6050a"}, + {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:67ecd894e56a0c6108ec5ab1d8fa8418ec0cff45844a855966b875d1039a2e34"}, + {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:14898830f0a0eb67cae2bbbc787c1a7d6e34ecc06fbd39d3af5fe29a4468e2c9"}, + {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:f2200e00b62568cfd920127782c61bc1c546062a879cdc741cfcc6976668dfcf"}, + {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9691a549c19c22d26a4f3b948071e93517bdf86e41b81d8c6ac8a964bb71e5a6"}, + {file = "regex-2023.8.8-cp39-cp39-win32.whl", hash = "sha256:6ab2ed84bf0137927846b37e882745a827458689eb969028af8032b1b3dac78e"}, + {file = "regex-2023.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5543c055d8ec7801901e1193a51570643d6a6ab8751b1f7dd9af71af467538bb"}, + {file = "regex-2023.8.8.tar.gz", hash = "sha256:fcbdc5f2b0f1cd0f6a56cdb46fe41d2cce1e644e3b68832f3eeebc5fb0f7712e"}, +] + [[package]] name = "requests" version = "2.28.2" @@ -3329,19 +3553,19 @@ tests = ["coverage[toml] (>=5.0.2)", "pytest"] [[package]] name = "setuptools" -version = "68.2.0" +version = "68.2.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.2.0-py3-none-any.whl", hash = "sha256:af3d5949030c3f493f550876b2fd1dd5ec66689c4ee5d5344f009746f71fd5a8"}, - {file = "setuptools-68.2.0.tar.gz", hash = "sha256:00478ca80aeebeecb2f288d3206b0de568df5cd2b8fada1209843cc9a8d88a48"}, + {file = "setuptools-68.2.1-py3-none-any.whl", hash = "sha256:eff96148eb336377ab11beee0c73ed84f1709a40c0b870298b0d058828761bae"}, + {file = "setuptools-68.2.1.tar.gz", hash = "sha256:56ee14884fd8d0cd015411f4a13f40b4356775a0aefd9ebc1d3bfb9a1acb32f1"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "sgmllib3k" @@ -3375,6 +3599,28 @@ files = [ {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] +[[package]] +name = "spotipy" +version = "2.23.0" +description = "A light weight Python library for the Spotify Web API" +optional = false +python-versions = "*" +files = [ + {file = "spotipy-2.23.0-py2-none-any.whl", hash = "sha256:da850fbf62faaa05912132d2886c293a5fbbe8350d0821e7208a6a2fdd6a0079"}, + {file = "spotipy-2.23.0-py3-none-any.whl", hash = "sha256:6bf8b963c10d0a3e51037e4baf92e29732dee36b2a1f1b7dcc8cd5771e662a5b"}, + {file = "spotipy-2.23.0.tar.gz", hash = "sha256:0dfafe08239daae6c16faa68f60b5775d40c4110725e1a7c545ad4c7fb66d4e8"}, +] + +[package.dependencies] +redis = ">=3.5.3" +requests = ">=2.25.0" +six = ">=1.15.0" +urllib3 = ">=1.26.0" + +[package.extras] +doc = ["Sphinx (>=1.5.2)"] +test = ["mock (==2.0.0)"] + [[package]] name = "sqlparse" version = "0.4.4" @@ -3442,6 +3688,32 @@ files = [ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] +[[package]] +name = "troi" +version = "0.1.0" +description = "An empathetic music recommendation system pipeline" +optional = false +python-versions = ">=3.6" +files = [] +develop = false + +[package.dependencies] +click = ">=8.0" +countryinfo = ">=0.1.2" +more_itertools = "*" +psycopg2-binary = ">=2.9.3" +pylistenbrainz = {git = "https://github.com/metabrainz/pylistenbrainz.git", rev = "v0.5.2"} +python-dateutil = ">=2.8.2" +requests = "*" +spotipy = ">=2.22.1" +ujson = ">=5.4.0" + +[package.source] +type = "git" +url = "https://github.com/metabrainz/troi-recommendation-playground.git" +reference = "main" +resolved_reference = "3c424ea4d7057ecc68d3b4b5a7036cf61bec816e" + [[package]] name = "twisted" version = "23.8.0" @@ -3550,6 +3822,76 @@ files = [ {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] +[[package]] +name = "ujson" +version = "5.8.0" +description = "Ultra fast JSON encoder and decoder for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ujson-5.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4511560d75b15ecb367eef561554959b9d49b6ec3b8d5634212f9fed74a6df1"}, + {file = "ujson-5.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9399eaa5d1931a0ead49dce3ffacbea63f3177978588b956036bfe53cdf6af75"}, + {file = "ujson-5.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4e7bb7eba0e1963f8b768f9c458ecb193e5bf6977090182e2b4f4408f35ac76"}, + {file = "ujson-5.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40931d7c08c4ce99adc4b409ddb1bbb01635a950e81239c2382cfe24251b127a"}, + {file = "ujson-5.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d53039d39de65360e924b511c7ca1a67b0975c34c015dd468fca492b11caa8f7"}, + {file = "ujson-5.8.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bdf04c6af3852161be9613e458a1fb67327910391de8ffedb8332e60800147a2"}, + {file = "ujson-5.8.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a70f776bda2e5072a086c02792c7863ba5833d565189e09fabbd04c8b4c3abba"}, + {file = "ujson-5.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f26629ac531d712f93192c233a74888bc8b8212558bd7d04c349125f10199fcf"}, + {file = "ujson-5.8.0-cp310-cp310-win32.whl", hash = "sha256:7ecc33b107ae88405aebdb8d82c13d6944be2331ebb04399134c03171509371a"}, + {file = "ujson-5.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:3b27a8da7a080add559a3b73ec9ebd52e82cc4419f7c6fb7266e62439a055ed0"}, + {file = "ujson-5.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:193349a998cd821483a25f5df30b44e8f495423840ee11b3b28df092ddfd0f7f"}, + {file = "ujson-5.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ddeabbc78b2aed531f167d1e70387b151900bc856d61e9325fcdfefb2a51ad8"}, + {file = "ujson-5.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ce24909a9c25062e60653073dd6d5e6ec9d6ad7ed6e0069450d5b673c854405"}, + {file = "ujson-5.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27a2a3c7620ebe43641e926a1062bc04e92dbe90d3501687957d71b4bdddaec4"}, + {file = "ujson-5.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b852bdf920fe9f84e2a2c210cc45f1b64f763b4f7d01468b33f7791698e455e"}, + {file = "ujson-5.8.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:20768961a6a706170497129960762ded9c89fb1c10db2989c56956b162e2a8a3"}, + {file = "ujson-5.8.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e0147d41e9fb5cd174207c4a2895c5e24813204499fd0839951d4c8784a23bf5"}, + {file = "ujson-5.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e3673053b036fd161ae7a5a33358ccae6793ee89fd499000204676baafd7b3aa"}, + {file = "ujson-5.8.0-cp311-cp311-win32.whl", hash = "sha256:a89cf3cd8bf33a37600431b7024a7ccf499db25f9f0b332947fbc79043aad879"}, + {file = "ujson-5.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3659deec9ab9eb19e8646932bfe6fe22730757c4addbe9d7d5544e879dc1b721"}, + {file = "ujson-5.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:102bf31c56f59538cccdfec45649780ae00657e86247c07edac434cb14d5388c"}, + {file = "ujson-5.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:299a312c3e85edee1178cb6453645217ba23b4e3186412677fa48e9a7f986de6"}, + {file = "ujson-5.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e385a7679b9088d7bc43a64811a7713cc7c33d032d020f757c54e7d41931ae"}, + {file = "ujson-5.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad24ec130855d4430a682c7a60ca0bc158f8253ec81feed4073801f6b6cb681b"}, + {file = "ujson-5.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16fde596d5e45bdf0d7de615346a102510ac8c405098e5595625015b0d4b5296"}, + {file = "ujson-5.8.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6d230d870d1ce03df915e694dcfa3f4e8714369cce2346686dbe0bc8e3f135e7"}, + {file = "ujson-5.8.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9571de0c53db5cbc265945e08f093f093af2c5a11e14772c72d8e37fceeedd08"}, + {file = "ujson-5.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7cba16b26efe774c096a5e822e4f27097b7c81ed6fb5264a2b3f5fd8784bab30"}, + {file = "ujson-5.8.0-cp312-cp312-win32.whl", hash = "sha256:48c7d373ff22366eecfa36a52b9b55b0ee5bd44c2b50e16084aa88b9de038916"}, + {file = "ujson-5.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ac97b1e182d81cf395ded620528c59f4177eee024b4b39a50cdd7b720fdeec6"}, + {file = "ujson-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2a64cc32bb4a436e5813b83f5aab0889927e5ea1788bf99b930fad853c5625cb"}, + {file = "ujson-5.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e54578fa8838ddc722539a752adfce9372474114f8c127bb316db5392d942f8b"}, + {file = "ujson-5.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9721cd112b5e4687cb4ade12a7b8af8b048d4991227ae8066d9c4b3a6642a582"}, + {file = "ujson-5.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d9707e5aacf63fb919f6237d6490c4e0244c7f8d3dc2a0f84d7dec5db7cb54c"}, + {file = "ujson-5.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0be81bae295f65a6896b0c9030b55a106fb2dec69ef877253a87bc7c9c5308f7"}, + {file = "ujson-5.8.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae7f4725c344bf437e9b881019c558416fe84ad9c6b67426416c131ad577df67"}, + {file = "ujson-5.8.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9ab282d67ef3097105552bf151438b551cc4bedb3f24d80fada830f2e132aeb9"}, + {file = "ujson-5.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:94c7bd9880fa33fcf7f6d7f4cc032e2371adee3c5dba2922b918987141d1bf07"}, + {file = "ujson-5.8.0-cp38-cp38-win32.whl", hash = "sha256:bf5737dbcfe0fa0ac8fa599eceafae86b376492c8f1e4b84e3adf765f03fb564"}, + {file = "ujson-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:11da6bed916f9bfacf13f4fc6a9594abd62b2bb115acfb17a77b0f03bee4cfd5"}, + {file = "ujson-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:69b3104a2603bab510497ceabc186ba40fef38ec731c0ccaa662e01ff94a985c"}, + {file = "ujson-5.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9249fdefeb021e00b46025e77feed89cd91ffe9b3a49415239103fc1d5d9c29a"}, + {file = "ujson-5.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2873d196725a8193f56dde527b322c4bc79ed97cd60f1d087826ac3290cf9207"}, + {file = "ujson-5.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4dafa9010c366589f55afb0fd67084acd8added1a51251008f9ff2c3e44042"}, + {file = "ujson-5.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a42baa647a50fa8bed53d4e242be61023bd37b93577f27f90ffe521ac9dc7a3"}, + {file = "ujson-5.8.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f3554eaadffe416c6f543af442066afa6549edbc34fe6a7719818c3e72ebfe95"}, + {file = "ujson-5.8.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fb87decf38cc82bcdea1d7511e73629e651bdec3a43ab40985167ab8449b769c"}, + {file = "ujson-5.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:407d60eb942c318482bbfb1e66be093308bb11617d41c613e33b4ce5be789adc"}, + {file = "ujson-5.8.0-cp39-cp39-win32.whl", hash = "sha256:0fe1b7edaf560ca6ab023f81cbeaf9946a240876a993b8c5a21a1c539171d903"}, + {file = "ujson-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:3f9b63530a5392eb687baff3989d0fb5f45194ae5b1ca8276282fb647f8dcdb3"}, + {file = "ujson-5.8.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:efeddf950fb15a832376c0c01d8d7713479fbeceaed1eaecb2665aa62c305aec"}, + {file = "ujson-5.8.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d8283ac5d03e65f488530c43d6610134309085b71db4f675e9cf5dff96a8282"}, + {file = "ujson-5.8.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb0142f6f10f57598655340a3b2c70ed4646cbe674191da195eb0985a9813b83"}, + {file = "ujson-5.8.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07d459aca895eb17eb463b00441986b021b9312c6c8cc1d06880925c7f51009c"}, + {file = "ujson-5.8.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d524a8c15cfc863705991d70bbec998456a42c405c291d0f84a74ad7f35c5109"}, + {file = "ujson-5.8.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d6f84a7a175c75beecde53a624881ff618e9433045a69fcfb5e154b73cdaa377"}, + {file = "ujson-5.8.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b748797131ac7b29826d1524db1cc366d2722ab7afacc2ce1287cdafccddbf1f"}, + {file = "ujson-5.8.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e72ba76313d48a1a3a42e7dc9d1db32ea93fac782ad8dde6f8b13e35c229130"}, + {file = "ujson-5.8.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f504117a39cb98abba4153bf0b46b4954cc5d62f6351a14660201500ba31fe7f"}, + {file = "ujson-5.8.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a8c91b6f4bf23f274af9002b128d133b735141e867109487d17e344d38b87d94"}, + {file = "ujson-5.8.0.tar.gz", hash = "sha256:78e318def4ade898a461b3d92a79f9441e7e0e4d2ad5419abed4336d702c7425"}, +] + [[package]] name = "unicode-slugify" version = "0.1.5" @@ -4097,4 +4439,4 @@ typesense = ["typesense"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "6f894585d8c11237f999c05c84a6ed5210e914e47dabaf4cccaba12cb58c65b7" +content-hash = "b5d18d34f29b2ac96360cae13fd38fbc14b37c0a44989b21c905820d2234ae9e" diff --git a/api/pyproject.toml b/api/pyproject.toml index 71416cad6..70e65c47f 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -84,6 +84,9 @@ requests = "==2.28.2" requests-http-message-signatures = "==0.3.1" sentry-sdk = "==1.19.1" watchdog = "==2.2.1" +troi = { git = "https://github.com/metabrainz/troi-recommendation-playground.git", branch = "main"} +lb-matching-tools = { git = "https://github.com/metabrainz/listenbrainz-matching-tools.git", branch = "main"} +unidecode = "==1.3.6" # Typesense typesense = { version = "==0.15.1", optional = true } diff --git a/api/tests/radios/test_lb_recommendations.py b/api/tests/radios/test_lb_recommendations.py new file mode 100644 index 000000000..d495a2e0e --- /dev/null +++ b/api/tests/radios/test_lb_recommendations.py @@ -0,0 +1,116 @@ +import pytest +import troi.core +from django.core.cache import cache +from django.db.models import Q +from requests.exceptions import ConnectTimeout + +from funkwhale_api.music.models import Track +from funkwhale_api.radios import lb_recommendations +from funkwhale_api.typesense import factories as custom_factories +from funkwhale_api.typesense import utils + + +def test_can_build_radio_queryset_with_fw_db(factories, mocker): + factories["music.Track"]( + title="I Want It That Way", mbid="87dfa566-21c3-45ed-bc42-1d345b8563fa" + ) + factories["music.Track"]( + title="The Perfect Kiss", mbid="ec0da94e-fbfe-4eb0-968e-024d4c32d1d0" + ) + factories["music.Track"]() + + qs = Track.objects.all() + + mocker.patch("funkwhale_api.typesense.utils.resolve_recordings_to_fw_track") + + radio_qs = lb_recommendations.build_radio_queryset( + custom_factories.DummyPatch(), {"min_recordings": 1}, qs + ) + recommended_recording_mbids = [ + "87dfa566-21c3-45ed-bc42-1d345b8563fa", + "ec0da94e-fbfe-4eb0-968e-024d4c32d1d0", + ] + + assert list( + Track.objects.all().filter(Q(mbid__in=recommended_recording_mbids)) + ) == list(radio_qs) + + +def test_build_radio_queryset_without_fw_db(mocker): + resolve_recordings_to_fw_track = mocker.patch.object( + utils, "resolve_recordings_to_fw_track", return_value=None + ) + # mocker.patch.object(cache, "get_many", return_value=None) + + qs = Track.objects.all() + + with pytest.raises(ValueError): + lb_recommendations.build_radio_queryset( + custom_factories.DummyPatch(), {"min_recordings": 1}, qs + ) + + assert resolve_recordings_to_fw_track.called_once_with( + custom_factories.recommended_recording_mbids + ) + + +def test_build_radio_queryset_with_redis_and_fw_db(factories, mocker): + factories["music.Track"]( + pk="1", title="I Want It That Way", mbid="87dfa566-21c3-45ed-bc42-1d345b8563fa" + ) + mocker.patch.object(utils, "resolve_recordings_to_fw_track", return_value=None) + redis_cache = {} + redis_cache["ec0da94e-fbfe-4eb0-968e-024d4c32d1d0"] = 2 + mocker.patch.object(cache, "get_many", return_value=redis_cache) + + qs = Track.objects.all() + + assert list( + lb_recommendations.build_radio_queryset( + custom_factories.DummyPatch(), {"min_recordings": 1}, qs + ) + ) == list(Track.objects.all().filter(pk__in=[1, 2])) + + +def test_build_radio_queryset_with_redis_and_without_fw_db(factories, mocker): + factories["music.Track"]( + pk="1", title="Super title", mbid="87dfaaaa-2aaa-45ed-bc42-1d34aaaaaaaa" + ) + mocker.patch.object(utils, "resolve_recordings_to_fw_track", return_value=None) + redis_cache = {} + redis_cache["87dfa566-21c3-45ed-bc42-1d345b8563fa"] = 1 + mocker.patch.object(cache, "get_many", return_value=redis_cache) + qs = Track.objects.all() + + assert list( + lb_recommendations.build_radio_queryset( + custom_factories.DummyPatch(), {"min_recordings": 1}, qs + ) + ) == list(Track.objects.all().filter(pk=1)) + + +def test_build_radio_queryset_catch_troi_ConnectTimeout(mocker): + mocker.patch.object( + troi.core, + "generate_playlist", + side_effect=ConnectTimeout, + ) + qs = Track.objects.all() + + with pytest.raises(ValueError): + lb_recommendations.build_radio_queryset( + custom_factories.DummyPatch(), {"min_recordings": 1}, qs + ) + + +def test_build_radio_queryset_catch_troi_no_candidates(mocker): + mocker.patch.object( + troi.core, + "generate_playlist", + ) + qs = Track.objects.all() + + with pytest.raises(ValueError): + lb_recommendations.build_radio_queryset( + custom_factories.DummyPatch(), {"min_recordings": 1}, qs + ) diff --git a/api/tests/radios/test_radios.py b/api/tests/radios/test_radios.py index edef108f4..1e9c02321 100644 --- a/api/tests/radios/test_radios.py +++ b/api/tests/radios/test_radios.py @@ -429,3 +429,28 @@ def test_can_start_custom_multiple_radio_from_api(api_client, factories): format="json", ) assert response.status_code == 201 + + +def test_can_start_periodic_jams_troi_radio_from_api(api_client, factories): + factories["music.Track"].create_batch(5) + url = reverse("api:v1:radios:sessions-list") + config = {"patch": "periodic-jams", "type": "daily-jams"} + response = api_client.post( + url, + {"radio_type": "troi", "config": config}, + format="json", + ) + assert response.status_code == 201 + + +# to do : send error to api ? +def test_can_catch_troi_radio_error(api_client, factories): + factories["music.Track"].create_batch(5) + url = reverse("api:v1:radios:sessions-list") + config = {"patch": "periodic-jams", "type": "not_existing_type"} + response = api_client.post( + url, + {"radio_type": "troi", "config": config}, + format="json", + ) + assert response.status_code == 201 diff --git a/api/tests/typesense/test_tasks.py b/api/tests/typesense/test_tasks.py new file mode 100644 index 000000000..9e3506a5c --- /dev/null +++ b/api/tests/typesense/test_tasks.py @@ -0,0 +1,58 @@ +import logging + +import requests_mock +import typesense + +from funkwhale_api.typesense import tasks + + +def test_add_tracks_to_index_fails(mocker, caplog): + logger = logging.getLogger("funkwhale_api.typesense.tasks") + caplog.set_level(logging.INFO) + logger.addHandler(caplog.handler) + + client = typesense.Client( + { + "api_key": "api_key", + "nodes": [{"host": "host", "port": "port", "protocol": "protocol"}], + "connection_timeout_seconds": 2, + } + ) + + with requests_mock.Mocker() as r_mocker: + r_mocker.post( + "protocol://host:port/collections/canonical_fw_data/documents/import", + json=[{"name": "data"}], + ) + mocker.patch.object(typesense, "Client", return_value=client) + mocker.patch.object( + typesense.client.ApiCall, + "post", + side_effect=typesense.exceptions.TypesenseClientError("Hello"), + ) + tasks.add_tracks_to_index([1, 2, 3]) + assert "Can't build index" in caplog.text + + +def test_build_canonical_index_success(mocker, caplog, factories): + logger = logging.getLogger("funkwhale_api.typesense.tasks") + caplog.set_level(logging.INFO) + logger.addHandler(caplog.handler) + + client = typesense.Client( + { + "api_key": "api_key", + "nodes": [{"host": "host", "port": "port", "protocol": "protocol"}], + "connection_timeout_seconds": 2, + } + ) + + factories["music.Track"].create_batch(size=5) + + with requests_mock.Mocker() as r_mocker: + mocker.patch.object(typesense, "Client", return_value=client) + + r_mocker.post("protocol://host:port/collections", json={"name": "data"}) + + tasks.build_canonical_index() + assert "Launching async task to add " in caplog.text diff --git a/api/tests/typesense/test_utils.py b/api/tests/typesense/test_utils.py new file mode 100644 index 000000000..f4927cecc --- /dev/null +++ b/api/tests/typesense/test_utils.py @@ -0,0 +1,43 @@ +import requests_mock +import typesense +from django.core.cache import cache + +from funkwhale_api.typesense import factories as custom_factories +from funkwhale_api.typesense import utils + + +def test_resolve_recordings_to_fw_track(mocker, factories): + artist = factories["music.Artist"](name="artist_name") + factories["music.Track"]( + pk=1, + title="I Want It That Way", + artist=artist, + mbid="87dfa566-21c3-45ed-bc42-1d345b8563fa", + ) + factories["music.Track"]( + pk=2, + title="I Want It That Way", + artist=artist, + ) + + client = typesense.Client( + { + "api_key": "api_key", + "nodes": [{"host": "host", "port": "port", "protocol": "protocol"}], + "connection_timeout_seconds": 2, + } + ) + with requests_mock.Mocker() as r_mocker: + mocker.patch.object(typesense, "Client", return_value=client) + mocker.patch.object( + typesense.client.ApiCall, + "post", + return_value=custom_factories.typesense_search_result, + ) + r_mocker.get( + "protocol://host:port/collections/canonical_fw_data/documents/search", + json=custom_factories.typesense_search_result, + ) + + utils.resolve_recordings_to_fw_track(custom_factories.recording_list) + assert cache.get("87dfa566-21c3-45ed-bc42-1d345b8563fa") == "1" diff --git a/changes/changelog.d/1861.newfeature b/changes/changelog.d/1861.newfeature new file mode 100644 index 000000000..762f3fb10 --- /dev/null +++ b/changes/changelog.d/1861.newfeature @@ -0,0 +1 @@ +Create a testing environment in production for ListenBrainz recommendation engine (troi-recommendation-playground) (#1861)