Compare commits

..

16 Commits

Author SHA1 Message Date
dependabot[bot]
868f5bf35d
Merge 9d974f9954 into 03c05d7adf 2025-03-31 05:12:26 +02:00
03c05d7adf updated redis 2025-01-16 21:43:05 +03:00
0b439f43a5 updated song search 2024-12-06 01:19:23 +03:00
6c15494aab updated song search 2024-12-05 19:02:03 +03:00
bf182dbd0a fixed song search 2024-12-05 18:45:59 +03:00
f268212094 updated local file load 2024-12-05 18:13:04 +03:00
f4ca7db696 updated search 2024-12-05 16:36:50 +03:00
600c903a68 added new functions for local upload 2024-12-05 06:25:43 +03:00
c3de9b45b8 fixed song process 2024-12-03 01:35:14 +03:00
66bd074149 fixed song process 2024-12-03 01:19:39 +03:00
b72ebe6e8c fixed song process 2024-12-03 00:53:26 +03:00
85e8e3fe8b fixed slug processing 2024-12-02 23:57:43 +03:00
2a7f1eae88 updated external service support 2024-12-02 20:57:19 +03:00
398820489f linter fix 2024-12-02 03:17:23 +03:00
a2607cd0d0 updated song search 2024-12-02 03:16:48 +03:00
1b9b1d2bb5 added external service for meta retrieve 2024-12-02 03:04:18 +03:00
16 changed files with 1368 additions and 817 deletions

View File

@ -7,8 +7,6 @@
Album,
AnonMusicUser,
Author,
MusicDraft,
MusicDraftFile,
Playlist,
PlaylistSong,
Song,
@ -90,8 +88,12 @@ def get_liked(self, obj):
@extend_schema_field(ListAlbumSerializer)
def get_album(self, obj):
if obj.album:
return ListAlbumSerializer(Album.objects.cache().get(id=obj.album_id)).data
if obj.album_id:
try:
album = Album.objects.cache().get(id=obj.album_id)
return ListAlbumSerializer(album).data
except Album.DoesNotExist:
return None
return None
@extend_schema_field(ListAuthorSerializer(many=True))
@ -107,16 +109,17 @@ def get_image(self, obj):
img = None
if obj.image_cropped:
img = obj.image_cropped
else:
album = Album.objects.cache().get(id=obj.album_id)
if album.image_cropped:
img = album.image_cropped
else:
authors = Author.objects.cache().filter(
Q(songs__id=obj.id) & ~Q(image="")
)
if authors:
img = authors.first().image_cropped
elif obj.album_id:
try:
album = Album.objects.cache().get(id=obj.album_id)
if album.image_cropped:
img = album.image_cropped
except Album.DoesNotExist:
pass
if not img:
authors = Author.objects.cache().filter(Q(songs__id=obj.id) & ~Q(image=""))
if authors.exists():
img = authors.first().image_cropped
if img:
return self.context["request"].build_absolute_uri(img.url)
return None
@ -406,35 +409,3 @@ def get_albums(self, obj):
return ListAlbumSerializer(
Album.objects.cache().search(obj["query"]).to_queryset()[:10], many=True
).data
class MusicDraftFileSerializer(serializers.ModelSerializer):
class Meta:
model = MusicDraftFile
fields = ["file", "original_name", "mime_type"]
class MusicDraftSerializer(serializers.ModelSerializer):
files = MusicDraftFileSerializer(many=True, read_only=True)
class Meta:
model = MusicDraft
fields = [
"id",
"status",
"provider",
"original_url",
"meta_data",
"file_token",
"created",
"updated",
"error_message",
"files",
]
read_only_fields = ["id", "file_token", "created", "updated"]
class MusicDraftCallbackSerializer(serializers.Serializer):
status = serializers.ChoiceField(choices=MusicDraft.STATUS_CHOICES)
meta_data = serializers.JSONField(required=False)
error_message = serializers.CharField(required=False)

View File

@ -16,8 +16,6 @@
ListSongPlaylistsAPIView,
ListSongSlugsAPIView,
ListUserListenedSongsAPIView,
MusicDraftCallbackView,
MusicDraftDetailView,
RemoveSongFromPlaylistAPIView,
RetrieveUpdateDestroyAlbumAPIView,
RetrieveUpdateDestroyAuthorAPIView,
@ -84,12 +82,4 @@
),
path("anon/create/", CreateAnonMusicUserAPIView.as_view(), name="create-anon"),
path("search/", SearchAllAPIView.as_view(), name="search_all"),
path(
"drafts/callback/<uuid:token>/",
MusicDraftCallbackView.as_view(),
name="draft-callback",
),
path(
"drafts/<str:file_token>/", MusicDraftDetailView.as_view(), name="draft-detail"
),
]

View File

@ -1,6 +1,5 @@
from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema
from rest_framework import generics, permissions, status
from rest_framework.generics import get_object_or_404
from rest_framework import generics, permissions
from rest_framework.response import Response
from akarpov.common.api.pagination import StandardResultsSetPagination
@ -19,22 +18,19 @@
ListPlaylistSerializer,
ListSongSerializer,
ListSongSlugsSerializer,
MusicDraftCallbackSerializer,
MusicDraftSerializer,
PlaylistSerializer,
SongSerializer,
)
from akarpov.music.models import (
Album,
Author,
MusicDraft,
Playlist,
Song,
SongUserRating,
UserListenHistory,
)
from akarpov.music.services.search import search_album, search_author, search_song
from akarpov.music.tasks import listen_to_song, process_draft_callback
from akarpov.music.tasks import listen_to_song
from akarpov.users.models import User
@ -546,38 +542,3 @@ def get(self, request, *args, **kwargs):
"authors": author_serializer.data,
}
)
class MusicDraftCallbackView(generics.GenericAPIView):
serializer_class = MusicDraftCallbackSerializer
@extend_schema(
description="Callback endpoint for external music service",
parameters=[
OpenApiParameter(
name="token",
type=str,
location=OpenApiParameter.PATH,
description="Draft callback token",
),
],
)
def post(self, request, token):
draft = get_object_or_404(MusicDraft, callback_token=token)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
process_draft_callback.delay(
draft_id=str(draft.id),
status=serializer.validated_data["status"],
meta_data=serializer.validated_data.get("meta_data"),
error_message=serializer.validated_data.get("error_message"),
)
return Response(status=status.HTTP_202_ACCEPTED)
class MusicDraftDetailView(generics.RetrieveAPIView):
queryset = MusicDraft.objects.all()
serializer_class = MusicDraftSerializer
lookup_field = "file_token"

View File

@ -11,7 +11,7 @@ class SongDocument(Document):
properties={
"name": fields.TextField(
fields={
"raw": fields.KeywordField(normalizer="lowercase"),
"raw": fields.KeywordField(normalizer="lowercase_normalizer"),
},
),
"name_transliterated": fields.TextField(
@ -21,7 +21,13 @@ class SongDocument(Document):
},
),
"link": fields.TextField(),
"meta": fields.ObjectField(dynamic=True),
"meta": fields.ObjectField(
dynamic=True,
properties={
"genre": fields.TextField(),
"release_year": fields.KeywordField(),
},
),
},
)
@ -30,7 +36,7 @@ class SongDocument(Document):
properties={
"name": fields.TextField(
fields={
"raw": fields.KeywordField(normalizer="lowercase"),
"raw": fields.KeywordField(normalizer="lowercase_normalizer"),
},
),
"name_transliterated": fields.TextField(
@ -40,7 +46,13 @@ class SongDocument(Document):
},
),
"link": fields.TextField(),
"meta": fields.ObjectField(dynamic=True),
"meta": fields.ObjectField(
dynamic=True,
properties={
"genre": fields.TextField(),
"release_year": fields.KeywordField(),
},
),
},
)
@ -48,7 +60,7 @@ class SongDocument(Document):
attr="name",
fields={
"raw": fields.KeywordField(),
"exact": fields.KeywordField(normalizer="lowercase"),
"exact": fields.KeywordField(normalizer="lowercase_normalizer"),
},
)
name_transliterated = fields.TextField(
@ -60,7 +72,13 @@ class SongDocument(Document):
)
suggest = fields.CompletionField()
meta = fields.ObjectField(dynamic=True)
meta = fields.ObjectField(
dynamic=True,
properties={
"genre": fields.TextField(),
"release_year": fields.KeywordField(),
},
)
class Index:
name = "songs"
@ -68,6 +86,13 @@ class Index:
"number_of_shards": 1,
"number_of_replicas": 0,
"analysis": {
"normalizer": {
"lowercase_normalizer": {
"type": "custom",
"char_filter": [],
"filter": ["lowercase"],
}
},
"filter": {
"my_transliterator": {
"type": "icu_transform",
@ -144,8 +169,8 @@ class Index:
"filter": [
"lowercase",
"autocomplete_filter",
"english_stemmer", # Apply English stemming for autocomplete
"russian_stemmer", # Include Russian stemming if applicable
"english_stemmer",
"russian_stemmer",
],
},
"search_synonym_with_stemming": {
@ -154,8 +179,8 @@ class Index:
"filter": [
"lowercase",
"synonym_filter",
"english_stemmer", # Apply English stemming for synonym search
"russian_stemmer", # Include Russian stemming if processing Russian synonyms
"english_stemmer",
"russian_stemmer",
],
},
},
@ -176,7 +201,7 @@ class AuthorDocument(Document):
name = fields.TextField(
fields={
"raw": fields.KeywordField(),
"exact": fields.KeywordField(normalizer="lowercase"),
"exact": fields.KeywordField(normalizer="lowercase_normalizer"),
},
)
name_transliterated = fields.TextField(
@ -187,11 +212,18 @@ class AuthorDocument(Document):
},
)
suggest = fields.CompletionField()
meta = fields.ObjectField(dynamic=True)
meta = fields.ObjectField(
dynamic=True,
properties={
"description": fields.TextField(),
# Ensure no empty date fields here either
"popularity": fields.IntegerField(),
},
)
class Index:
name = "authors"
settings = SongDocument.Index.settings # Reuse settings
settings = SongDocument.Index.settings
class Django:
model = Author
@ -202,7 +234,7 @@ class AlbumDocument(Document):
name = fields.TextField(
fields={
"raw": fields.KeywordField(),
"exact": fields.KeywordField(normalizer="lowercase"),
"exact": fields.KeywordField(normalizer="lowercase_normalizer"),
},
)
name_transliterated = fields.TextField(
@ -213,7 +245,13 @@ class AlbumDocument(Document):
},
)
suggest = fields.CompletionField()
meta = fields.ObjectField(dynamic=True)
meta = fields.ObjectField(
dynamic=True,
properties={
"genre": fields.TextField(),
"release_year": fields.KeywordField(),
},
)
authors = fields.NestedField(
attr="authors",
properties={
@ -223,7 +261,6 @@ class AlbumDocument(Document):
},
),
"name_transliterated": fields.TextField(
attr="name",
analyzer="transliterate",
fields={
"raw": fields.KeywordField(),
@ -236,7 +273,7 @@ class AlbumDocument(Document):
class Index:
name = "albums"
settings = SongDocument.Index.settings # Reuse settings
settings = SongDocument.Index.settings
class Django:
model = Album

View File

@ -1,89 +0,0 @@
# Generated by Django 4.2.16 on 2024-10-26 10:37
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
("music", "0016_anonmusicuser_song_created_song_volume_and_more"),
]
operations = [
migrations.CreateModel(
name="MusicDraft",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"status",
models.CharField(
choices=[
("pending", "Pending"),
("processing", "Processing"),
("failed", "Failed"),
("complete", "Complete"),
],
default="pending",
max_length=20,
),
),
(
"provider",
models.CharField(
choices=[
("spotify", "Spotify"),
("yandex", "Yandex"),
("youtube", "YouTube"),
],
max_length=20,
),
),
("original_url", models.URLField()),
("meta_data", models.JSONField(blank=True, null=True)),
("file_token", models.CharField(max_length=100, unique=True)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
("error_message", models.TextField(blank=True, null=True)),
("user_id", models.IntegerField(null=True)),
(
"callback_token",
models.UUIDField(default=uuid.uuid4, editable=False),
),
],
),
migrations.CreateModel(
name="MusicDraftFile",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("file", models.FileField(upload_to="music_drafts/")),
("original_name", models.CharField(max_length=255)),
("mime_type", models.CharField(max_length=100)),
("created", models.DateTimeField(auto_now_add=True)),
(
"draft",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="files",
to="music.musicdraft",
),
),
],
),
]

View File

@ -1,7 +1,6 @@
import uuid
from django.contrib.postgres.fields import ArrayField
from django.contrib.sites.models import Site
from django.db import models
from django.urls import reverse
@ -99,50 +98,6 @@ class SlugMeta:
slug_length = 10
class MusicDraft(models.Model):
STATUS_CHOICES = (
("pending", "Pending"),
("processing", "Processing"),
("failed", "Failed"),
("complete", "Complete"),
)
PROVIDER_CHOICES = (
("spotify", "Spotify"),
("yandex", "Yandex"),
("youtube", "YouTube"),
)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending")
provider = models.CharField(max_length=20, choices=PROVIDER_CHOICES)
original_url = models.URLField()
meta_data = models.JSONField(null=True, blank=True)
file_token = models.CharField(max_length=100, unique=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
error_message = models.TextField(null=True, blank=True)
user_id = models.IntegerField(null=True)
callback_token = models.UUIDField(default=uuid.uuid4, editable=False)
def get_callback_url(self):
site = Site.objects.get_current()
path = reverse(
"music:api:draft-callback", kwargs={"token": self.callback_token}
)
return f"https://{site.domain}{path}"
class MusicDraftFile(models.Model):
draft = models.ForeignKey(
MusicDraft, on_delete=models.CASCADE, related_name="files"
)
file = models.FileField(upload_to="music_drafts/")
original_name = models.CharField(max_length=255)
mime_type = models.CharField(max_length=100)
created = models.DateTimeField(auto_now_add=True)
class Playlist(ShortLinkModel, UserHistoryModel):
name = models.CharField(max_length=200)
private = models.BooleanField(default=True)

View File

@ -8,7 +8,7 @@
except requests.exceptions.JSONDecodeError:
print("Failed to initialize GoogleTranslator due to external API issues.")
from django.core.files import File
from django.db import transaction
from django.db import IntegrityError, transaction
from django.utils.text import slugify
from mutagen import File as MutagenFile
from mutagen.id3 import APIC, ID3, TCON, TORY, TextFrame
@ -19,14 +19,64 @@
from akarpov.music.models import Album, Author, Song
from akarpov.music.services.info import generate_readable_slug, search_all_platforms
from akarpov.users.models import User
from akarpov.utils.generators import generate_charset # Import generate_charset
def get_or_create_author(author_name):
"""Get or create author with unique slug."""
with transaction.atomic():
author = Author.objects.filter(name__iexact=author_name).order_by("id").first()
if author is None:
author = Author.objects.create(name=author_name)
return author
for attempt in range(5):
try:
slug = generate_readable_slug(author_name, Author)
author = Author.objects.create(name=author_name, slug=slug)
return author
except IntegrityError:
# Slug conflict, retry slug generation
continue
else:
# If we still fail, get the existing author
author = (
Author.objects.filter(name__iexact=author_name)
.order_by("id")
.first()
)
if author:
return author
else:
raise Exception("Failed to create or get author")
else:
return author
def get_or_create_album(album_name):
"""Get or create album with unique slug."""
if not album_name:
return None
with transaction.atomic():
album = Album.objects.filter(name__iexact=album_name).order_by("id").first()
if album is None:
for attempt in range(5):
try:
slug = generate_readable_slug(album_name, Album)
album = Album.objects.create(name=album_name, slug=slug)
return album
except IntegrityError:
# Slug conflict, retry slug generation
continue
else:
# If we still fail, get the existing album
album = (
Album.objects.filter(name__iexact=album_name).order_by("id").first()
)
if album:
return album
else:
raise Exception("Failed to create or get album")
else:
return album
def process_track_name(track_name: str) -> str:
@ -98,20 +148,18 @@ def load_track(
kwargs["release"] if "release" in kwargs else search_info.get("release", None)
)
if album and type(album) is str and album.startswith("['"):
if album and isinstance(album, str) and album.startswith("['"):
album = album.replace("['", "").replace("']", "")
if album:
if type(album) is str:
if isinstance(album, str):
album_name = album
elif type(album) is list:
elif isinstance(album, list):
album_name = album[0]
else:
album_name = None
if album_name:
album, created = Album.objects.get_or_create(
name__iexact=album_name, defaults={"name": album_name}
)
album = get_or_create_album(album_name)
processed_authors = []
if authors:
@ -121,7 +169,7 @@ def load_track(
authors = processed_authors
if sng := Song.objects.filter(
name=name if name else p_name,
name__iexact=name if name else p_name,
authors__id__in=[x.id for x in authors],
album=album,
):
@ -186,15 +234,29 @@ def load_track(
new_file_name = generated_name + ".mp3"
if image_path:
with open(path, "rb") as file, open(image_path, "rb") as image:
song.image = File(image, name=generated_name + ".png")
song.file = File(file, name=new_file_name)
song.save()
# Generate unique slug for the song
song.slug = generate_readable_slug(name if name else p_name, Song)
# Try to save the song, handling potential slug conflicts
for attempt in range(5):
try:
if image_path:
with open(path, "rb") as file, open(image_path, "rb") as image:
song.image = File(image, name=generated_name + ".png")
song.file = File(file, name=new_file_name)
song.save()
else:
with open(path, "rb") as file:
song.file = File(file, name=new_file_name)
song.save()
break # Successfully saved the song
except IntegrityError:
# Slug conflict, generate a new slug using generate_charset
song.slug = generate_readable_slug(
song.name + "_" + generate_charset(5), Song
)
else:
with open(path, "rb") as file:
song.file = File(file, name=new_file_name)
song.save()
raise Exception("Failed to save song with unique slug after multiple attempts")
if not album.image and song.image:
album.image = song.image
@ -203,7 +265,7 @@ def load_track(
if authors:
song.authors.set([x.id for x in authors])
# set music meta
# Set music metadata
tag = MutagenFile(song.file.path)
tag["title"] = TextFrame(encoding=3, text=[name])
if album:
@ -236,7 +298,4 @@ def load_track(
if os.path.exists(image_path):
os.remove(image_path)
song.slug = generate_readable_slug(song.name, Song)
song.save()
return song

View File

@ -1,84 +0,0 @@
import os
from django.core.files import File
from akarpov.music.models import Album, Author, MusicDraft, Song
def save_song_from_draft(draft: MusicDraft) -> Song | None:
"""
Create a Song instance from a completed MusicDraft
"""
try:
if not draft.files.exists():
draft.status = "failed"
draft.error_message = "No files associated with draft"
draft.save()
return None
# Get the music file
draft_file = draft.files.filter(mime_type__startswith="audio/").first()
if not draft_file:
draft.status = "failed"
draft.error_message = "No audio file found in draft"
draft.save()
return None
# Get metadata from draft
meta_data = draft.meta_data or {}
# Create song instance
song = Song(
name=meta_data.get("title", ""),
length=meta_data.get("length"),
link=draft.original_url,
meta=meta_data,
)
# Handle album
if "album" in meta_data:
album, _ = Album.objects.get_or_create(
name=meta_data["album"].get("name", ""),
defaults={"meta": meta_data["album"]},
)
song.album = album
# Save song to get an ID
with open(draft_file.file.path, "rb") as f:
song.file.save(
os.path.basename(draft_file.original_name), File(f), save=True
)
# Handle authors
if "artists" in meta_data:
authors = []
for artist_data in meta_data["artists"]:
author, _ = Author.objects.get_or_create(
name=artist_data.get("name", ""), defaults={"meta": artist_data}
)
authors.append(author)
song.authors.set(authors)
# Handle image if present
image_file = draft.files.filter(mime_type__startswith="image/").first()
if image_file:
with open(image_file.file.path, "rb") as f:
song.image.save(
os.path.basename(image_file.original_name), File(f), save=True
)
# Add user if specified
if draft.user_id:
song.creator_id = draft.user_id
song.save()
# Clean up draft files
draft.delete()
return song
except Exception as e:
draft.status = "failed"
draft.error_message = str(e)
draft.save()
return None

View File

@ -0,0 +1,144 @@
import os
from functools import wraps
from random import randint
from typing import Any
import requests
import structlog
from django.conf import settings
logger = structlog.get_logger(__name__)
class ExternalServiceClient:
def __init__(self):
self.base_url = getattr(settings, "MUSIC_EXTERNAL_SERVICE_URL", None)
def _make_request(
self, endpoint: str, params: dict = None, **kwargs
) -> dict[str, Any] | None:
if not self.base_url:
return None
url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}"
try:
response = requests.post(url, params=params, **kwargs)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(
"External service request failed",
error=str(e),
endpoint=endpoint,
params=params,
)
return None
def get_spotify_info(self, track_name: str) -> dict[str, Any] | None:
response = self._make_request("/spotify/search", params={"query": track_name})
if not response:
return None
if "album_image" in response:
# Download and save image
image_path = self._download_image(response["album_image"])
if image_path:
response["album_image_path"] = image_path
return response
def get_spotify_album_info(self, album_name: str) -> dict[str, Any] | None:
response = self._make_request("/spotify/album", params={"query": album_name})
if not response:
return None
if response.get("images"):
image_path = self._download_image(response["images"][0].get("url"))
if image_path:
response["image_path"] = image_path
return response
def get_spotify_artist_info(self, artist_name: str) -> dict[str, Any] | None:
response = self._make_request("/spotify/artist", params={"query": artist_name})
if not response:
return None
if response.get("images"):
image_path = self._download_image(response["images"][0].get("url"))
if image_path:
response["image_path"] = image_path
return response
def _download_image(self, url: str) -> str | None:
if not url:
return None
try:
response = requests.get(url)
if response.status_code == 200:
image_path = os.path.join(
settings.MEDIA_ROOT, f"tmp_{randint(10000, 99999)}.png"
)
with open(image_path, "wb") as f:
f.write(response.content)
return image_path
except Exception as e:
logger.error("Failed to download image", error=str(e), url=url)
return None
def translate_text(
self, text: str, source_lang: str = "auto", target_lang: str = "en"
) -> str | None:
response = self._make_request(
"/translation/translate",
json={"text": text, "source_lang": source_lang, "target_lang": target_lang},
)
return response.get("translated_text") if response else None
def external_service_fallback(fallback_func):
"""Decorator to try external service first, then fall back to local implementation"""
@wraps(fallback_func)
def wrapper(*args, **kwargs):
if (
not hasattr(settings, "MUSIC_EXTERNAL_SERVICE_URL")
or not settings.MUSIC_EXTERNAL_SERVICE_URL
):
return fallback_func(*args, **kwargs)
client = ExternalServiceClient()
try:
if fallback_func.__name__ == "get_spotify_info":
result = client.get_spotify_info(args[1]) # args[1] is track_name
elif fallback_func.__name__ == "get_spotify_album_info":
result = client.get_spotify_album_info(args[0]) # args[0] is album_name
elif fallback_func.__name__ == "get_spotify_artist_info":
result = client.get_spotify_artist_info(
args[0]
) # args[0] is artist_name
elif fallback_func.__name__ == "safe_translate":
result = client.translate_text(args[0]) # args[0] is text
else:
logger.warning(
"Unknown function for external service fallback",
function=fallback_func.__name__,
)
return fallback_func(*args, **kwargs)
if result:
return result
except Exception as e:
logger.error(
"External service failed, falling back to local implementation",
error=str(e),
function=fallback_func.__name__,
args=args,
)
return fallback_func(*args, **kwargs)
return wrapper

View File

@ -1,4 +1,5 @@
import os
import time
from io import BytesIO
from pathlib import Path
from random import randint
@ -6,11 +7,19 @@
import librosa
import mutagen
import numpy as np
from django.core.files import File
from django.db import IntegrityError, transaction
from django.db.models import Count
from django.utils.text import slugify
from mutagen.id3 import ID3
from mutagen.mp3 import MP3
from PIL import Image, UnidentifiedImageError
from akarpov.music.models import Song
from akarpov.common.tasks import crop_model_image
from akarpov.music.models import Album, Author, Song
from akarpov.music.services.db import load_track
from akarpov.users.models import User
from akarpov.utils.generators import generate_charset
def load_dir(path: str, user_id: int):
@ -83,3 +92,493 @@ def set_song_volume(song: Song):
mp3_file = song.file.path
song.volume = analyze_music_loudness(mp3_file)
song.save(update_fields=["volume"])
BATCH_SIZE = 10
BATCH_CHECK_DELAY = 10 # seconds
class FileProcessor:
def __init__(self):
self.failed_files: list[str] = []
self.processed_files: set[str] = set()
self.current_batch: dict[str, dict] = {}
def load_dir(self, path: str, user_id: int) -> tuple[list[str], int]:
path = Path(path)
files = list(path.glob("**/*.mp3"))
total_files = len(files)
for i in range(0, len(files), BATCH_SIZE):
batch = files[i : i + BATCH_SIZE] # noqa
self._process_batch(batch, user_id)
# Wait and verify batch
time.sleep(BATCH_CHECK_DELAY)
self._verify_batch()
print(
"Batch processed",
processed=len(self.processed_files),
failed=len(self.failed_files),
total=total_files,
remaining=total_files
- len(self.processed_files)
- len(self.failed_files),
)
return self.failed_files, len(self.processed_files)
def _process_batch(self, files: list[Path], user_id: int):
self.current_batch.clear()
for file_path in files:
file_str = str(file_path)
if file_str in self.processed_files or file_str in self.failed_files:
continue
try:
file_info = self._extract_file_info(file_str)
if self._check_exists(file_info):
self.processed_files.add(file_str)
continue
self.current_batch[file_str] = file_info
self._process_file(file_str, file_info, user_id)
except Exception as e:
print("File processing failed", file=file_str, error=str(e))
self.failed_files.append(file_str)
def _verify_batch(self):
for file_path, info in self.current_batch.items():
if not self._verify_file(file_path, info):
print("File verification failed", file=file_path)
self.failed_files.append(file_path)
else:
self.processed_files.add(file_path)
def _extract_file_info(self, path: str) -> dict:
tag = mutagen.File(path, easy=True)
return {
"author": tag.get("artist"),
"album": tag.get("album"),
"name": tag.get("title", [path.split("/")[-1]])[0],
"image": self._extract_image(path),
}
def _extract_image(self, path: str) -> str | None:
try:
tags = ID3(path)
pict = [x for x in tags.getall("APIC") if x]
if not pict:
return None
pict_data = pict[0].data
im = Image.open(BytesIO(pict_data))
image_path = f"/tmp/{randint(1, 1000000)}.png"
while os.path.exists(image_path):
image_path = f"/tmp/{randint(1, 1000000)}.png"
im.save(image_path)
return image_path
except (UnidentifiedImageError, Exception) as e:
print("Image extraction failed", error=str(e))
return None
def _check_exists(self, info: dict) -> bool:
query = Song.objects.filter(name=info["name"])
if info["author"]:
query = query.filter(authors__name__in=info["author"])
if info["album"]:
query = query.filter(album__name=info["album"])
return query.exists()
def _verify_file(self, file_path: str, info: dict) -> bool:
song = Song.objects.filter(name=info["name"], file__isnull=False).first()
if not song:
return False
# Verify file exists and is readable
if not os.path.exists(song.file.path):
return False
# Verify image if it was expected
if info["image"] and not song.image:
return False
# Verify metadata
if info["author"]:
if not song.authors.filter(name__in=info["author"]).exists():
return False
if info["album"]:
if not song.album or song.album.name != info["album"]:
return False
return True
def _process_file(self, path: str, info: dict, user_id: int):
try:
song = load_track(
path=path,
image_path=info["image"],
user_id=user_id,
authors=info["author"],
album=info["album"],
name=info["name"],
)
if info["image"] and os.path.exists(info["image"]):
os.remove(info["image"])
set_song_volume(song)
except Exception as e:
print("File processing failed", file=path, error=str(e))
self.failed_files.append(path)
def clean_title_for_slug(title: str) -> str:
"""Clean title for slug generation."""
# Remove common suffixes
suffixes = [
"(Original Mix)",
"(Radio Edit)",
"(Extended Mix)",
"(Official Video)",
"(Music Video)",
"(Lyric Video)",
"(Audio)",
"(Official Audio)",
"(Visualizer)",
"(Official Music Video)",
"(Official Lyric Video)",
]
cleaned = title
for suffix in suffixes:
cleaned = cleaned.replace(suffix, "")
return cleaned.strip()
def process_authors_string(authors_str: str) -> list[str]:
"""Split author string into individual author names."""
if not authors_str:
return []
# First split by major separators
authors = []
for part in authors_str.split("/"):
for subpart in part.split("&"):
# Handle various featuring cases
for feat_marker in [
" feat.",
" ft.",
" featuring.",
" presents ",
" pres. ",
]:
if feat_marker in subpart.lower():
parts = subpart.lower().split(feat_marker, 1)
authors.extend(part.strip() for part in parts)
break
else:
# Handle collaboration markers
if " x " in subpart:
authors.extend(p.strip() for p in subpart.split(" x "))
else:
authors.append(subpart.strip())
# Remove duplicates while preserving order
seen = set()
return [x for x in authors if not (x.lower() in seen or seen.add(x.lower()))]
def extract_mp3_metadata(file_path: str) -> dict | None:
"""Extract metadata from MP3 file."""
try:
audio = MP3(file_path, ID3=ID3)
tags = audio.tags if audio.tags else {}
# Get filename without extension for fallback
base_filename = os.path.splitext(os.path.basename(file_path))[0]
metadata = {
"title": None,
"album": None,
"artists": None,
"genre": None,
"release_year": None,
"length": audio.info.length,
"image_data": None,
"image_mime": None,
}
if tags:
# Extract basic metadata with fallbacks
metadata["title"] = str(tags.get("TIT2", "")) or base_filename
metadata["album"] = str(tags.get("TALB", ""))
metadata["artists"] = str(tags.get("TPE1", "")) or str(tags.get("TPE2", ""))
metadata["genre"] = str(tags.get("TCON", ""))
metadata["release_year"] = str(tags.get("TDRC", "")) or str(
tags.get("TYER", "")
)
# Extract cover art
for tag in tags.getall("APIC"):
if tag.type == 3: # Front cover
metadata["image_data"] = tag.data
metadata["image_mime"] = tag.mime
break
# Clean up title if it came from filename
if metadata["title"] == base_filename:
parts = base_filename.split(" - ", 1)
if len(parts) > 1 and not metadata["artists"]:
metadata["artists"] = parts[0]
metadata["title"] = parts[1]
return metadata
except Exception as e:
print(f"Error extracting metadata from {file_path}: {str(e)}")
return None
def generate_unique_slug(
base_name: str, model_class, existing_id=None, max_length=20
) -> str:
"""Generate a unique slug for a model instance."""
# Clean and slugify the base name
slug = slugify(clean_title_for_slug(base_name))
# Truncate if necessary
if len(slug) > max_length:
slug = slug[:max_length].rsplit("-", 1)[0]
original_slug = slug
counter = 1
# Check for uniqueness
while True:
if existing_id:
exists = (
model_class.objects.filter(slug=slug).exclude(id=existing_id).exists()
)
else:
exists = model_class.objects.filter(slug=slug).exists()
if not exists:
break
# Generate new slug
if counter == 1:
if len(original_slug) > (max_length - 7): # Leave room for _XXXXX
base = original_slug[: (max_length - 7)]
else:
base = original_slug
slug = f"{base}_{generate_charset(5)}"
else:
if len(original_slug) > (max_length - len(str(counter)) - 1):
base = original_slug[: (max_length - len(str(counter)) - 1)]
else:
base = original_slug
slug = f"{base}_{counter}"
counter += 1
return slug
def save_image_as_png(image_data: bytes, mime_type: str) -> str | None:
"""Convert image data to PNG and save temporarily."""
try:
if not image_data:
return None
img = Image.open(BytesIO(image_data))
temp_path = f"/tmp/{generate_charset(10)}.png"
img.save(temp_path, "PNG")
return temp_path
except Exception as e:
print(f"Error processing image: {str(e)}")
return None
def get_or_create_album(album_name: str, authors: list[Author]) -> Album | None:
"""Get or create album with proper locking and uniqueness check."""
if not album_name:
return None
with transaction.atomic():
# Try to find existing album
album = (
Album.objects.select_for_update().filter(name__iexact=album_name).first()
)
if album:
# Add any missing authors
current_author_ids = set(album.authors.values_list("id", flat=True))
new_author_ids = {author.id for author in authors}
missing_authors = new_author_ids - current_author_ids
if missing_authors:
album.authors.add(*missing_authors)
return album
try:
# Create new album
album = Album.objects.create(
name=album_name, slug=generate_unique_slug(album_name, Album)
)
album.authors.set(authors)
return album
except IntegrityError:
# Handle race condition
album = (
Album.objects.select_for_update()
.filter(name__iexact=album_name)
.first()
)
if album:
album.authors.add(*authors)
return album
raise
def check_song_exists(title: str, album: Album | None, authors: list[Author]) -> bool:
"""Check if a song already exists with the given title, album and authors."""
query = Song.objects.filter(name__iexact=title)
if album:
query = query.filter(album=album)
if authors:
# Ensure exact author match
query = query.annotate(author_count=Count("authors")).filter(
author_count=len(authors), authors__in=authors
)
return query.exists()
def load_mp3_directory(
directory_path: str, user_id: int | None = None
) -> tuple[list[str], int]:
"""
Load all MP3 files from a directory and its subdirectories.
Returns tuple of (failed_files, processed_count)
"""
path = Path(directory_path)
failed_files = []
processed_count = 0
# Keep track of created/updated objects for image cropping
created_authors = set()
created_albums = set()
created_songs = set()
for mp3_path in path.glob("**/*.mp3"):
try:
metadata = extract_mp3_metadata(str(mp3_path))
if not metadata:
failed_files.append(str(mp3_path))
continue
with transaction.atomic():
# Process authors
author_names = process_authors_string(metadata["artists"])
authors = []
for author_name in author_names:
author = Author.objects.filter(name__iexact=author_name).first()
if not author:
author = Author.objects.create(
name=author_name,
slug=generate_unique_slug(author_name, Author),
)
created_authors.add(author.id)
authors.append(author)
# Process album
album = None
if metadata["album"]:
try:
album = get_or_create_album(metadata["album"], authors)
if album.id not in created_albums and not album.image_cropped:
created_albums.add(album.id)
except IntegrityError as e:
print(f"Error creating album for {mp3_path}: {str(e)}")
failed_files.append(str(mp3_path))
continue
# Check for existing song
if check_song_exists(metadata["title"], album, authors):
print(
f"Skipping existing song: {metadata['artists']} - {metadata['title']}"
)
continue
# Process cover image
temp_image_path = None
if metadata["image_data"]:
temp_image_path = save_image_as_png(
metadata["image_data"], metadata["image_mime"]
)
if album and not album.image and temp_image_path:
with open(temp_image_path, "rb") as img_file:
album.image.save(
f"{album.slug}.png", File(img_file), save=True
)
# Create song with proper slug from file name
file_name = os.path.splitext(os.path.basename(str(mp3_path)))[0]
song = Song(
name=metadata["title"],
length=metadata["length"],
album=album,
slug=generate_unique_slug(file_name, Song),
creator=User.objects.get(id=user_id) if user_id else None,
meta={
"genre": metadata["genre"],
"release_year": metadata["release_year"],
},
)
# Save files
with open(mp3_path, "rb") as mp3_file:
song.file.save(f"{song.slug}.mp3", File(mp3_file), save=True)
if temp_image_path:
with open(temp_image_path, "rb") as img_file:
song.image.save(f"{song.slug}.png", File(img_file), save=True)
os.remove(temp_image_path)
# Set authors
song.authors.set(authors)
created_songs.add(song.id)
processed_count += 1
print(f"Successfully processed: {song.artists_names} - {song.name}")
except Exception as e:
print(f"Error processing {mp3_path}: {str(e)}")
failed_files.append(str(mp3_path))
# Trigger image cropping for all created/updated objects
print("Processing image cropping...")
for author_id in created_authors:
author = Author.objects.get(id=author_id)
if author.image and not author.image_cropped:
print(f"Cropping image for author: {author.name}")
crop_model_image(author_id, "music", "author")
for album_id in created_albums:
album = Album.objects.get(id=album_id)
if album.image and not album.image_cropped:
print(f"Cropping image for album: {album.name}")
crop_model_image(album_id, "music", "album")
for song_id in created_songs:
song = Song.objects.get(id=song_id)
if song.image and not song.image_cropped:
print(f"Cropping image for song: {song.name}")
crop_model_image(song_id, "music", "song")
return failed_files, processed_count

View File

@ -1,9 +1,16 @@
import os
import re
from random import randint
from typing import Any
import requests
import spotipy
from akarpov.music.services.external import (
ExternalServiceClient,
external_service_fallback,
)
try:
from deep_translator import GoogleTranslator
except requests.exceptions.JSONDecodeError:
@ -22,35 +29,74 @@
from akarpov.utils.text import is_similar_artist, normalize_text
def clean_name(name: str) -> str:
# Replace special characters with underscores
cleaned = name.strip().replace(" ", "_")
cleaned = re.sub(r"[^\w\-]", "_", cleaned)
# Remove consecutive underscores
cleaned = re.sub(r"_+", "_", cleaned)
# Remove trailing underscores
cleaned = cleaned.strip("_")
return cleaned
def split_authors(authors_str: str) -> list[str]:
# Split on common separators
if not authors_str:
return []
# First split by obvious delimiters
authors = []
for part in re.split(r"[,/&]", authors_str):
# Clean up each part
cleaned = part.strip()
if " feat." in cleaned.lower():
# Split on featuring
main_artist, feat_artist = cleaned.lower().split(" feat.", 1)
authors.extend([main_artist.strip(), feat_artist.strip()])
elif " ft." in cleaned.lower():
main_artist, feat_artist = cleaned.lower().split(" ft.", 1)
authors.extend([main_artist.strip(), feat_artist.strip()])
elif " x " in cleaned:
# Split artist collaborations
authors.extend(p.strip() for p in cleaned.split(" x "))
elif cleaned:
authors.append(cleaned)
# Remove duplicates while preserving order
seen = set()
return [x for x in authors if not (x in seen or seen.add(x))]
def generate_readable_slug(name: str, model: Model) -> str:
# Translate and slugify the name
# Clean and translate name
slug = safe_translate(name)
# Truncate slug if it's too long
if len(slug) > 20:
slug = slug[:20]
last_dash = slug.rfind("-")
if last_dash != -1:
slug = slug[:last_dash]
# Remove any remaining spaces and ensure proper formatting
slug = clean_name(slug)
original_slug = slug
# Truncate if necessary
if len(slug) > 20:
# Try to cut at word boundary
truncated = slug[:20].rsplit("_", 1)[0]
slug = truncated if truncated else slug[:20]
original_slug = slug.lower()
# Ensure uniqueness
counter = 1
while model.objects.filter(slug=slug).exists():
while model.objects.filter(slug__iexact=slug).exists():
if len(original_slug) > 14:
truncated_slug = original_slug[:14]
last_dash = truncated_slug.rfind("-")
if last_dash != -1:
truncated_slug = truncated_slug[:last_dash]
truncated = original_slug[:14].rsplit("_", 1)[0]
base_slug = truncated if truncated else original_slug[:14]
else:
truncated_slug = original_slug
base_slug = original_slug
suffix = f"_{generate_charset(5)}" if counter == 1 else f"_{counter}"
slug = f"{truncated_slug}{suffix}"
slug = f"{base_slug}{suffix}"
counter += 1
return slug
return slug.lower()
def create_spotify_session() -> spotipy.Spotify:
@ -76,6 +122,19 @@ def spotify_search(name: str, session: spotipy.Spotify, search_type="track"):
return res
def clean_spotify_response(data: dict[str, Any]) -> dict[str, Any]:
if isinstance(data, dict):
return {
k: clean_spotify_response(v)
for k, v in data.items()
if k != "available_markets"
}
elif isinstance(data, list):
return [clean_spotify_response(item) for item in data]
return data
@external_service_fallback
def get_spotify_info(name: str, session: spotipy.Spotify) -> dict:
info = {
"album_name": "",
@ -84,7 +143,11 @@ def get_spotify_info(name: str, session: spotipy.Spotify) -> dict:
"artists": [],
"artist": "",
"title": "",
"genre": "",
"genre": [],
"meta": {},
"album_meta": {},
"external_urls": {},
"full_data": {},
}
try:
@ -93,32 +156,47 @@ def get_spotify_info(name: str, session: spotipy.Spotify) -> dict:
return info
track = results[0]
artist_data = session.artist(track["artists"][0]["external_urls"]["spotify"])
album_data = session.album(track["album"]["id"])
info.update(
{
"album_name": track["album"]["name"],
"album_image": track["album"]["images"][0]["url"]
if track["album"]["images"]
else "",
"release": track["album"]["release_date"].split("-")[0],
"album_image": track["album"]["images"][0]["url"],
"artists": [artist["name"] for artist in track["artists"]],
"artist": track["artists"][0]["name"],
"title": track["name"],
# Extract additional data as needed
"genre": artist_data.get("genres", []),
"meta": {
"duration_ms": track.get("duration_ms"),
"explicit": track.get("explicit"),
"popularity": track.get("popularity"),
"preview_url": track.get("preview_url"),
"track_number": track.get("track_number"),
"type": track.get("type"),
},
"album_meta": clean_spotify_response(album_data),
"external_urls": track.get("external_urls", {}),
"full_data": clean_spotify_response(track),
}
)
artist_data = session.artist(track["artists"][0]["external_urls"]["spotify"])
info["genre"] = artist_data.get("genres", [])
if track["album"]["images"]:
album_image_url = track["album"]["images"][0]["url"]
image_response = requests.get(album_image_url)
if image_response.status_code == 200:
image_path = os.path.join(
settings.MEDIA_ROOT, f"tmp_{randint(10000, 99999)}.png"
)
with open(image_path, "wb") as f:
f.write(image_response.content)
info["album_image_path"] = image_path
album_image_url = track["album"]["images"][0]["url"]
image_response = requests.get(album_image_url)
if image_response.status_code == 200:
image_path = os.path.join(
settings.MEDIA_ROOT, f"tmp_{randint(10000, 99999)}.png"
)
with open(image_path, "wb") as f:
f.write(image_response.content)
info["album_image_path"] = image_path
except Exception:
except Exception as e:
print("Failed to get Spotify info", error=str(e))
return info
return info
@ -172,20 +250,101 @@ def search_yandex(name: str):
return info
def get_spotify_album_info(album_name: str, session: spotipy.Spotify):
search_result = session.search(q="album:" + album_name, type="album")
albums = search_result.get("albums", {}).get("items", [])
if albums:
return albums[0]
return None
@external_service_fallback
def get_spotify_album_info(album_name: str, session: spotipy.Spotify) -> dict:
info = {
"name": "",
"link": "",
"meta": {},
"image_url": "",
"release_date": "",
"total_tracks": 0,
"images": [],
"external_urls": {},
"artists": [],
"genres": [],
"tracks": [],
"full_data": {},
}
try:
search_result = session.search(q="album:" + album_name, type="album")
albums = search_result.get("albums", {}).get("items", [])
if not albums:
return info
album = albums[0]
album_id = album["id"]
full_album = session.album(album_id)
tracks = session.album_tracks(album_id)
return {
"name": album.get("name", ""),
"link": album.get("external_urls", {}).get("spotify", ""),
"meta": {
"album_type": album.get("album_type", ""),
"release_date_precision": album.get("release_date_precision", ""),
"total_tracks": album.get("total_tracks", 0),
"type": album.get("type", ""),
},
"image_url": next(
(img["url"] for img in album.get("images", []) if img.get("url")), ""
),
"release_date": album.get("release_date", ""),
"total_tracks": album.get("total_tracks", 0),
"images": album.get("images", []),
"external_urls": album.get("external_urls", {}),
"artists": clean_spotify_response(album.get("artists", [])),
"genres": clean_spotify_response(full_album.get("genres", [])),
"tracks": clean_spotify_response(tracks.get("items", [])),
"full_data": clean_spotify_response(full_album),
}
except Exception as e:
print("Failed to get album info", error=str(e))
return info
def get_spotify_artist_info(artist_name: str, session: spotipy.Spotify):
search_result = session.search(q="artist:" + artist_name, type="artist")
artists = search_result.get("artists", {}).get("items", [])
if artists:
return artists[0]
return None
@external_service_fallback
def get_spotify_artist_info(artist_name: str, session: spotipy.Spotify) -> dict:
info = {
"name": "",
"link": "",
"meta": {},
"image_url": "",
"genres": [],
"popularity": 0,
"images": [],
"external_urls": {},
"full_data": {},
}
try:
search_result = session.search(q="artist:" + artist_name, type="artist")
artists = search_result.get("artists", {}).get("items", [])
if not artists:
return info
artist = artists[0]
return {
"name": artist.get("name", ""),
"link": artist.get("external_urls", {}).get("spotify", ""),
"meta": {
"followers": artist.get("followers", {}).get("total", 0),
"popularity": artist.get("popularity", 0),
"type": artist.get("type", ""),
},
"image_url": next(
(img["url"] for img in artist.get("images", []) if img.get("url")), ""
),
"genres": artist.get("genres", []),
"popularity": artist.get("popularity", 0),
"images": artist.get("images", []),
"external_urls": artist.get("external_urls", {}),
"full_data": clean_spotify_response(artist),
}
except Exception as e:
print("Failed to get artist info", error=str(e))
return info
def get_yandex_album_info(album_name: str, client: Client):
@ -379,29 +538,46 @@ def save_author_image(author, image_path):
print(f"Error saving author image: {str(e)}")
def safe_translate(text):
@external_service_fallback
def safe_translate(text: str) -> str:
try:
text = clean_name(text) # Clean before translation
translated = GoogleTranslator(source="auto", target="en").translate(text)
return slugify(translated)
# Clean after translation and ensure proper slugification
return slugify(clean_name(translated)).replace(" ", "_").lower()
except Exception as e:
print(f"Error translating text: {str(e)}")
return slugify(text)
print(f"Translation failed: {str(e)}")
# Fallback to direct slugification
return slugify(clean_name(text)).replace(" ", "_").lower()
def search_all_platforms(track_name: str) -> dict:
print(track_name)
# session = spotipy.Spotify(
# auth_manager=spotipy.SpotifyClientCredentials(
# client_id=settings.MUSIC_SPOTIFY_ID,
# client_secret=settings.MUSIC_SPOTIFY_SECRET,
# )
# )
# spotify_info = get_spotify_info(track_name, session)
spotify_info = {} # TODO: add proxy for info retrieve
if settings.MUSIC_EXTERNAL_SERVICE_URL:
# Use external service if configured
client = ExternalServiceClient()
spotify_info = client.get_spotify_info(track_name) or {}
else:
# Local implementation fallback
try:
session = spotipy.Spotify(
auth_manager=SpotifyClientCredentials(
client_id=settings.MUSIC_SPOTIFY_ID,
client_secret=settings.MUSIC_SPOTIFY_SECRET,
)
)
spotify_info = get_spotify_info(track_name, session)
except Exception as e:
print("Local Spotify implementation failed", error=str(e))
spotify_info = {}
yandex_info = search_yandex(track_name)
if "album_image_path" in spotify_info and "album_image_path" in yandex_info:
os.remove(yandex_info["album_image_path"])
# Combine artist information
combined_artists = set()
for artist in spotify_info.get("artists", []) + yandex_info.get("artists", []):
normalized_artist = normalize_text(artist)
@ -410,12 +586,13 @@ def search_all_platforms(track_name: str) -> dict:
for existing_artist in combined_artists
):
combined_artists.add(normalized_artist)
genre = spotify_info.get("genre") or yandex_info.get("genre")
if type(genre) is list:
genre = sorted(genre, key=lambda x: len(x))
genre = genre[0]
track_info = {
# Process genre information
genre = spotify_info.get("genre") or yandex_info.get("genre")
if isinstance(genre, list) and genre:
genre = sorted(genre, key=len)[0]
return {
"album_name": spotify_info.get("album_name")
or yandex_info.get("album_name", ""),
"release": spotify_info.get("release") or yandex_info.get("release", ""),
@ -425,5 +602,3 @@ def search_all_platforms(track_name: str) -> dict:
"album_image": spotify_info.get("album_image_path")
or yandex_info.get("album_image_path", None),
}
return track_info

View File

@ -8,66 +8,85 @@
def search_song(query):
if not query:
return Song.objects.none()
search = SongDocument.search()
should_queries = [
ES_Q("match_phrase", name={"query": query, "boost": 5}),
# Priorities:
# 1. Exact phrase matches in name, author name, album name
# 2. Part of author/album name
# 3. Exact name (exact matches)
# 4. Fuzzy matches
# 5. Wildcards
# phrase matches (highest priority)
phrase_queries = [
ES_Q("match_phrase", name={"query": query, "boost": 10}),
ES_Q(
"nested",
path="authors",
query=ES_Q("match_phrase", name={"query": query, "boost": 4}),
query=ES_Q("match_phrase", authors__name={"query": query, "boost": 9}),
),
ES_Q(
"nested",
path="album",
query=ES_Q("match_phrase", name={"query": query, "boost": 4}),
),
ES_Q("match", name={"query": query, "fuzziness": "AUTO", "boost": 3}),
ES_Q(
"nested",
path="authors",
query=ES_Q("match", name={"query": query, "fuzziness": "AUTO", "boost": 2}),
),
ES_Q(
"nested",
path="album",
query=ES_Q("match", name={"query": query, "fuzziness": "AUTO", "boost": 2}),
),
ES_Q("wildcard", name={"value": f"*{query.lower()}*", "boost": 1}),
ES_Q(
"nested",
path="authors",
query=ES_Q("wildcard", name={"value": f"*{query.lower()}*", "boost": 0.8}),
),
ES_Q(
"nested",
path="album",
query=ES_Q("wildcard", name={"value": f"*{query.lower()}*", "boost": 0.8}),
),
ES_Q(
"match",
name_transliterated={"query": query, "fuzziness": "AUTO", "boost": 1},
query=ES_Q("match_phrase", album__name={"query": query, "boost": 9}),
),
]
# exact keyword matches (non-case sensitive due to normalizers)
exact_queries = [
ES_Q("term", **{"name.exact": {"value": query.lower(), "boost": 8}})
]
# fuzzy matches
fuzzy_queries = [
ES_Q("match", name={"query": query, "fuzziness": "AUTO", "boost": 5}),
ES_Q(
"nested",
path="authors",
query=ES_Q(
"match",
name_transliterated={"query": query, "fuzziness": "AUTO", "boost": 0.8},
"match", authors__name={"query": query, "fuzziness": "AUTO", "boost": 4}
),
),
ES_Q(
"nested",
path="album",
query=ES_Q(
"match",
name_transliterated={"query": query, "fuzziness": "AUTO", "boost": 0.8},
"match", album__name={"query": query, "fuzziness": "AUTO", "boost": 4}
),
),
]
search_query = ES_Q("bool", should=should_queries, minimum_should_match=1)
# wildcard matches
wildcard_queries = [
ES_Q("wildcard", name={"value": f"*{query.lower()}*", "boost": 2}),
ES_Q(
"nested",
path="authors",
query=ES_Q(
"wildcard", authors__name={"value": f"*{query.lower()}*", "boost": 2}
),
),
ES_Q(
"nested",
path="album",
query=ES_Q(
"wildcard", album__name={"value": f"*{query.lower()}*", "boost": 2}
),
),
]
# Combine queries
# We'll use a should query to incorporate all of these, relying on boosting
search_query = ES_Q(
"bool",
should=phrase_queries + exact_queries + fuzzy_queries + wildcard_queries,
minimum_should_match=1,
)
# Execute search with size limit
search = search.query(search_query).extra(size=20)
response = search.execute()
@ -103,8 +122,9 @@ def bulk_update_index(model_class):
def search_author(query):
if not query:
return Author.objects.none()
search = AuthorDocument.search()
should_queries = [
ES_Q("match_phrase", name={"query": query, "boost": 5}),
ES_Q("match", name={"query": query, "fuzziness": "AUTO", "boost": 3}),
@ -114,7 +134,6 @@ def search_author(query):
name_transliterated={"query": query, "fuzziness": "AUTO", "boost": 1},
),
]
search_query = ES_Q("bool", should=should_queries, minimum_should_match=1)
search = search.query(search_query).extra(size=10)
response = search.execute()
@ -125,11 +144,12 @@ def search_author(query):
Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(hit_ids)])
)
return authors
return Author.objects.none()
def search_album(query):
if not query:
return Album.objects.none()
search = AlbumDocument.search()
should_queries = [
@ -141,7 +161,6 @@ def search_album(query):
name_transliterated={"query": query, "fuzziness": "AUTO", "boost": 1},
),
]
search_query = ES_Q("bool", should=should_queries, minimum_should_match=1)
search = search.query(search_query).extra(size=10)
response = search.execute()
@ -152,5 +171,4 @@ def search_album(query):
Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(hit_ids)])
)
return albums
return Album.objects.none()

View File

@ -2,14 +2,12 @@
from urllib.parse import parse_qs, urlparse
import pylast
import requests
import spotipy
import structlog
from asgiref.sync import async_to_sync
from celery import shared_task
from channels.layers import get_channel_layer
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.timezone import now
from spotipy import SpotifyClientCredentials
@ -19,14 +17,12 @@
from akarpov.music.models import (
AnonMusicUser,
AnonMusicUserHistory,
MusicDraft,
RadioSong,
Song,
UserListenHistory,
UserMusicProfile,
)
from akarpov.music.services import spotify, yandex, youtube
from akarpov.music.services.drafts import save_song_from_draft
from akarpov.music.services.file import load_dir, load_file
from akarpov.utils.celery import get_scheduled_tasks_name
@ -34,31 +30,20 @@
@shared_task(soft_time_limit=60 * 60, time_limit=60 * 120)
def list_tracks(self, url: str, user_id: int | None = None) -> str | None:
"""Update list_tracks to handle failures"""
try:
url = normalize_url(url)
handlers = {
"spotify.com": handle_spotify,
"music.yandex.ru": handle_yandex,
"youtube.com": handle_youtube,
}
def list_tracks(url, user_id):
url = normalize_url(url)
for domain, handler in handlers.items():
if domain in url:
return handler(url, user_id)
handlers = {
"spotify.com": handle_spotify,
"music.yandex.ru": handle_yandex,
"youtube.com": handle_youtube,
}
return fallback_search(url, user_id)
except Exception as e:
draft = MusicDraft.objects.create(
provider="unknown",
original_url=url,
user_id=user_id,
status="pending",
error_message=str(e),
)
handle_download_failure.delay(str(draft.id), self.request.id)
return None
for domain, handler in handlers.items():
if domain in url:
return handler(url, user_id)
print("failed to find handler, falling back to search")
return fallback_search(url, user_id)
def normalize_url(url):
@ -67,22 +52,24 @@ def normalize_url(url):
)
def handle_spotify(url: str, user_id: int | None = None) -> str | None:
return download_spotify_url.delay(url, user_id)
def handle_spotify(url, user_id):
spotify.download_url(url, user_id)
return url
def handle_yandex(url: str, user_id: int | None = None) -> str | None:
return load_yandex_url.delay(url, user_id)
def handle_yandex(url, user_id):
yandex.load_url(url, user_id)
return url
def handle_youtube(url: str, user_id: int | None = None) -> str | None:
"""Handle YouTube downloads"""
def handle_youtube(url, user_id):
if "channel" in url or "/c/" in url:
return handle_youtube_channel(url, user_id)
elif "playlist" in url or "&list=" in url:
return handle_youtube_playlist(url, user_id)
else:
return process_yb.delay(url, user_id)
process_yb.apply_async(kwargs={"url": url, "user_id": user_id})
return url
def handle_youtube_channel(url, user_id):
@ -139,53 +126,10 @@ def fallback_search(url, user_id):
return url
@shared_task(bind=True)
def process_yb(self, url: str, user_id: int | None = None) -> str | None:
"""Update YouTube download to handle failures"""
try:
return str(youtube.download_from_youtube_link(url, user_id))
except Exception as e:
draft = MusicDraft.objects.create(
provider="youtube",
original_url=url,
user_id=user_id,
status="pending",
error_message=str(e),
)
handle_download_failure.delay(str(draft.id), self.request.id)
return None
@shared_task(bind=True)
def download_spotify_url(self, url: str, user_id: int | None = None) -> str | None:
try:
return spotify.download_url(url, user_id)
except Exception as e:
draft = MusicDraft.objects.create(
provider="spotify",
original_url=url,
user_id=user_id,
status="pending",
error_message=str(e),
)
handle_download_failure.delay(str(draft.id), self.request.id)
return None
@shared_task(bind=True)
def load_yandex_url(self, url: str, user_id: int | None = None) -> str | None:
try:
return yandex.load_url(url, user_id)
except Exception as e:
draft = MusicDraft.objects.create(
provider="yandex",
original_url=url,
user_id=user_id,
status="pending",
error_message=str(e),
)
handle_download_failure.delay(str(draft.id), self.request.id)
return None
@shared_task(max_retries=5)
def process_yb(url, user_id):
youtube.download_from_youtube_link(url, user_id)
return url
@shared_task
@ -316,53 +260,3 @@ def listen_to_song(song_id, user_id=None, anon=True):
except Exception as e:
logger.error(f"Last.fm scrobble error: {e}")
return song_id
@shared_task
def handle_download_failure(draft_id: str, original_task_id: str):
"""
Handle failed downloads by sending request to external service
"""
draft = MusicDraft.objects.get(id=draft_id)
external_service_url = "http://music-download-service/api/v1/download"
response = requests.post(
external_service_url,
json={
"url": draft.original_url,
"provider": draft.provider,
"callback_url": draft.get_callback_url(),
"file_token": draft.file_token,
},
)
if response.status_code != 202:
draft.status = "failed"
draft.error_message = f"External service request failed: {response.text}"
draft.save()
return False
draft.status = "processing"
draft.save()
return True
@shared_task
def process_draft_callback(
draft_id: str, status: str, meta_data: dict = None, error_message: str = None
):
"""
Process callback from external service
"""
draft = get_object_or_404(MusicDraft, id=draft_id)
draft.status = status
if meta_data:
draft.meta_data = meta_data
if error_message:
draft.error_message = error_message
draft.save()
if status == "complete":
save_song_from_draft(draft)

View File

@ -715,6 +715,9 @@
LAST_FM_API_KEY = env("LAST_FM_API_KET", default="")
LAST_FM_SECRET = env("LAST_FM_SECRET", default="")
# EXTERNAL
MUSIC_EXTERNAL_SERVICE_URL = env("MUSIC_EXTERNAL_SERVICE_URL", default="")
# ROBOTS
# ------------------------------------------------------------------------------
ROBOTS_USE_SITEMAP = True

View File

@ -69,6 +69,11 @@ services:
redis:
image: redis:6
container_name: akarpov_local_redis
command: redis-server --maxmemory 4gb --maxmemory-policy allkeys-lru
deploy:
resources:
limits:
memory: 4G
celeryworker:
<<: *django

469
poetry.lock generated
View File

@ -1577,13 +1577,13 @@ tzdata = "*"
[[package]]
name = "django-ckeditor-5"
version = "0.2.15"
version = "0.2.14"
description = "CKEditor 5 for Django."
optional = false
python-versions = ">=3.7"
files = [
{file = "django_ckeditor_5-0.2.15-py3-none-any.whl", hash = "sha256:71980998d4bef7b272e50cb6878cca5c0bedec42b389a3827ffe1bbb88babd29"},
{file = "django_ckeditor_5-0.2.15.tar.gz", hash = "sha256:587f0485a3dca6a1a7c7c68e4160770e46389b8cf50205bf352bd47fcb63567c"},
{file = "django_ckeditor_5-0.2.14-py3-none-any.whl", hash = "sha256:ed3be0c0381a181e013507ed39a89d73d8a8da9cde670915f071b3dde6b93c56"},
{file = "django_ckeditor_5-0.2.14.tar.gz", hash = "sha256:bc7709588077a12feb934373d4091bb38c1ed7b4ec7867fcda4896600c0a2f3c"},
]
[package.dependencies]
@ -2205,13 +2205,13 @@ doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"]
[[package]]
name = "faker"
version = "30.6.0"
version = "30.3.0"
description = "Faker is a Python package that generates fake data for you."
optional = false
python-versions = ">=3.8"
files = [
{file = "Faker-30.6.0-py3-none-any.whl", hash = "sha256:37b5ab951f7367ea93edb865120e9717a7a649d6a4b223f1e4a47a8a20d9e85f"},
{file = "faker-30.6.0.tar.gz", hash = "sha256:be0e548352c1be6f6d9c982003848a0d305868f160bb1fb7f945acffc347e676"},
{file = "Faker-30.3.0-py3-none-any.whl", hash = "sha256:e8a15fd1b0f72992b008f5ea94c70d3baa0cb51b0d5a0e899c17b1d1b23d2771"},
{file = "faker-30.3.0.tar.gz", hash = "sha256:8760fbb34564fbb2f394345eef24aec5b8f6506b6cfcefe8195ed66dd1032bdb"},
]
[package.dependencies]
@ -4424,78 +4424,83 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
[[package]]
name = "psycopg2-binary"
version = "2.9.10"
version = "2.9.9"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.7"
files = [
{file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"},
{file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"},
]
[[package]]
@ -4734,13 +4739,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pydantic-settings"
version = "2.6.0"
version = "2.5.2"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_settings-2.6.0-py3-none-any.whl", hash = "sha256:4a819166f119b74d7f8c765196b165f95cc7487ce58ea27dec8a5a26be0970e0"},
{file = "pydantic_settings-2.6.0.tar.gz", hash = "sha256:44a1804abffac9e6a30372bb45f6cafab945ef5af25e66b1c634c01dd39e0188"},
{file = "pydantic_settings-2.5.2-py3-none-any.whl", hash = "sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907"},
{file = "pydantic_settings-2.5.2.tar.gz", hash = "sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0"},
]
[package.dependencies]
@ -6222,68 +6227,60 @@ test = ["mock (==2.0.0)"]
[[package]]
name = "sqlalchemy"
version = "2.0.36"
version = "2.0.35"
description = "Database Abstraction Library"
optional = false
python-versions = ">=3.7"
files = [
{file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72"},
{file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908"},
{file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08"},
{file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07"},
{file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5"},
{file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44"},
{file = "SQLAlchemy-2.0.36-cp310-cp310-win32.whl", hash = "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa"},
{file = "SQLAlchemy-2.0.36-cp310-cp310-win_amd64.whl", hash = "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5"},
{file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c"},
{file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71"},
{file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff"},
{file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d"},
{file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb"},
{file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8"},
{file = "SQLAlchemy-2.0.36-cp311-cp311-win32.whl", hash = "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f"},
{file = "SQLAlchemy-2.0.36-cp311-cp311-win_amd64.whl", hash = "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959"},
{file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4"},
{file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855"},
{file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53"},
{file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a"},
{file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686"},
{file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588"},
{file = "SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e"},
{file = "SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5"},
{file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef"},
{file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8"},
{file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b"},
{file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2"},
{file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf"},
{file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c"},
{file = "SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436"},
{file = "SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88"},
{file = "SQLAlchemy-2.0.36-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b"},
{file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d"},
{file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971"},
{file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2"},
{file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575"},
{file = "SQLAlchemy-2.0.36-cp37-cp37m-win32.whl", hash = "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c"},
{file = "SQLAlchemy-2.0.36-cp37-cp37m-win_amd64.whl", hash = "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb"},
{file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545"},
{file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24"},
{file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3"},
{file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687"},
{file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346"},
{file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1"},
{file = "SQLAlchemy-2.0.36-cp38-cp38-win32.whl", hash = "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e"},
{file = "SQLAlchemy-2.0.36-cp38-cp38-win_amd64.whl", hash = "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793"},
{file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa"},
{file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689"},
{file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d"},
{file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06"},
{file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763"},
{file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7"},
{file = "SQLAlchemy-2.0.36-cp39-cp39-win32.whl", hash = "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28"},
{file = "SQLAlchemy-2.0.36-cp39-cp39-win_amd64.whl", hash = "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a"},
{file = "SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e"},
{file = "sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5"},
{file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67219632be22f14750f0d1c70e62f204ba69d28f62fd6432ba05ab295853de9b"},
{file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4668bd8faf7e5b71c0319407b608f278f279668f358857dbfd10ef1954ac9f90"},
{file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8bea573863762bbf45d1e13f87c2d2fd32cee2dbd50d050f83f87429c9e1ea"},
{file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f552023710d4b93d8fb29a91fadf97de89c5926c6bd758897875435f2a939f33"},
{file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:016b2e665f778f13d3c438651dd4de244214b527a275e0acf1d44c05bc6026a9"},
{file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7befc148de64b6060937231cbff8d01ccf0bfd75aa26383ffdf8d82b12ec04ff"},
{file = "SQLAlchemy-2.0.35-cp310-cp310-win32.whl", hash = "sha256:22b83aed390e3099584b839b93f80a0f4a95ee7f48270c97c90acd40ee646f0b"},
{file = "SQLAlchemy-2.0.35-cp310-cp310-win_amd64.whl", hash = "sha256:a29762cd3d116585278ffb2e5b8cc311fb095ea278b96feef28d0b423154858e"},
{file = "SQLAlchemy-2.0.35-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e21f66748ab725ade40fa7af8ec8b5019c68ab00b929f6643e1b1af461eddb60"},
{file = "SQLAlchemy-2.0.35-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a6219108a15fc6d24de499d0d515c7235c617b2540d97116b663dade1a54d62"},
{file = "SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042622a5306c23b972192283f4e22372da3b8ddf5f7aac1cc5d9c9b222ab3ff6"},
{file = "SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:627dee0c280eea91aed87b20a1f849e9ae2fe719d52cbf847c0e0ea34464b3f7"},
{file = "SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fdcd72a789c1c31ed242fd8c1bcd9ea186a98ee8e5408a50e610edfef980d71"},
{file = "SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89b64cd8898a3a6f642db4eb7b26d1b28a497d4022eccd7717ca066823e9fb01"},
{file = "SQLAlchemy-2.0.35-cp311-cp311-win32.whl", hash = "sha256:6a93c5a0dfe8d34951e8a6f499a9479ffb9258123551fa007fc708ae2ac2bc5e"},
{file = "SQLAlchemy-2.0.35-cp311-cp311-win_amd64.whl", hash = "sha256:c68fe3fcde03920c46697585620135b4ecfdfc1ed23e75cc2c2ae9f8502c10b8"},
{file = "SQLAlchemy-2.0.35-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eb60b026d8ad0c97917cb81d3662d0b39b8ff1335e3fabb24984c6acd0c900a2"},
{file = "SQLAlchemy-2.0.35-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6921ee01caf375363be5e9ae70d08ce7ca9d7e0e8983183080211a062d299468"},
{file = "SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cdf1a0dbe5ced887a9b127da4ffd7354e9c1a3b9bb330dce84df6b70ccb3a8d"},
{file = "SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93a71c8601e823236ac0e5d087e4f397874a421017b3318fd92c0b14acf2b6db"},
{file = "SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e04b622bb8a88f10e439084486f2f6349bf4d50605ac3e445869c7ea5cf0fa8c"},
{file = "SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1b56961e2d31389aaadf4906d453859f35302b4eb818d34a26fab72596076bb8"},
{file = "SQLAlchemy-2.0.35-cp312-cp312-win32.whl", hash = "sha256:0f9f3f9a3763b9c4deb8c5d09c4cc52ffe49f9876af41cc1b2ad0138878453cf"},
{file = "SQLAlchemy-2.0.35-cp312-cp312-win_amd64.whl", hash = "sha256:25b0f63e7fcc2a6290cb5f7f5b4fc4047843504983a28856ce9b35d8f7de03cc"},
{file = "SQLAlchemy-2.0.35-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f021d334f2ca692523aaf7bbf7592ceff70c8594fad853416a81d66b35e3abf9"},
{file = "SQLAlchemy-2.0.35-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05c3f58cf91683102f2f0265c0db3bd3892e9eedabe059720492dbaa4f922da1"},
{file = "SQLAlchemy-2.0.35-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:032d979ce77a6c2432653322ba4cbeabf5a6837f704d16fa38b5a05d8e21fa00"},
{file = "SQLAlchemy-2.0.35-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:2e795c2f7d7249b75bb5f479b432a51b59041580d20599d4e112b5f2046437a3"},
{file = "SQLAlchemy-2.0.35-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:cc32b2990fc34380ec2f6195f33a76b6cdaa9eecf09f0c9404b74fc120aef36f"},
{file = "SQLAlchemy-2.0.35-cp37-cp37m-win32.whl", hash = "sha256:9509c4123491d0e63fb5e16199e09f8e262066e58903e84615c301dde8fa2e87"},
{file = "SQLAlchemy-2.0.35-cp37-cp37m-win_amd64.whl", hash = "sha256:3655af10ebcc0f1e4e06c5900bb33e080d6a1fa4228f502121f28a3b1753cde5"},
{file = "SQLAlchemy-2.0.35-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4c31943b61ed8fdd63dfd12ccc919f2bf95eefca133767db6fbbd15da62078ec"},
{file = "SQLAlchemy-2.0.35-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a62dd5d7cc8626a3634208df458c5fe4f21200d96a74d122c83bc2015b333bc1"},
{file = "SQLAlchemy-2.0.35-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0630774b0977804fba4b6bbea6852ab56c14965a2b0c7fc7282c5f7d90a1ae72"},
{file = "SQLAlchemy-2.0.35-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d625eddf7efeba2abfd9c014a22c0f6b3796e0ffb48f5d5ab106568ef01ff5a"},
{file = "SQLAlchemy-2.0.35-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ada603db10bb865bbe591939de854faf2c60f43c9b763e90f653224138f910d9"},
{file = "SQLAlchemy-2.0.35-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c41411e192f8d3ea39ea70e0fae48762cd11a2244e03751a98bd3c0ca9a4e936"},
{file = "SQLAlchemy-2.0.35-cp38-cp38-win32.whl", hash = "sha256:d299797d75cd747e7797b1b41817111406b8b10a4f88b6e8fe5b5e59598b43b0"},
{file = "SQLAlchemy-2.0.35-cp38-cp38-win_amd64.whl", hash = "sha256:0375a141e1c0878103eb3d719eb6d5aa444b490c96f3fedab8471c7f6ffe70ee"},
{file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ccae5de2a0140d8be6838c331604f91d6fafd0735dbdcee1ac78fc8fbaba76b4"},
{file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2a275a806f73e849e1c309ac11108ea1a14cd7058577aba962cd7190e27c9e3c"},
{file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:732e026240cdd1c1b2e3ac515c7a23820430ed94292ce33806a95869c46bd139"},
{file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890da8cd1941fa3dab28c5bac3b9da8502e7e366f895b3b8e500896f12f94d11"},
{file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0d8326269dbf944b9201911b0d9f3dc524d64779a07518199a58384c3d37a44"},
{file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b76d63495b0508ab9fc23f8152bac63205d2a704cd009a2b0722f4c8e0cba8e0"},
{file = "SQLAlchemy-2.0.35-cp39-cp39-win32.whl", hash = "sha256:69683e02e8a9de37f17985905a5eca18ad651bf592314b4d3d799029797d0eb3"},
{file = "SQLAlchemy-2.0.35-cp39-cp39-win_amd64.whl", hash = "sha256:aee110e4ef3c528f3abbc3c2018c121e708938adeeff9006428dd7c8555e9b3f"},
{file = "SQLAlchemy-2.0.35-py3-none-any.whl", hash = "sha256:2ab3f0336c0387662ce6221ad30ab3a5e6499aab01b9790879b6578fd9b8faa1"},
{file = "sqlalchemy-2.0.35.tar.gz", hash = "sha256:e11d7ea4d24f0a262bccf9a7cd6284c976c5369dac21db237cff59586045ab9f"},
]
[package.dependencies]
@ -6296,7 +6293,7 @@ aioodbc = ["aioodbc", "greenlet (!=0.4.17)"]
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
asyncio = ["greenlet (!=0.4.17)"]
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"]
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
mssql = ["pyodbc"]
mssql-pymssql = ["pymssql"]
mssql-pyodbc = ["pyodbc"]
@ -6608,13 +6605,13 @@ files = [
[[package]]
name = "types-requests"
version = "2.32.0.20241016"
version = "2.32.0.20240914"
description = "Typing stubs for requests"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"},
{file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"},
{file = "types-requests-2.32.0.20240914.tar.gz", hash = "sha256:2850e178db3919d9bf809e434eef65ba49d0e7e33ac92d588f4a5e295fffd405"},
{file = "types_requests-2.32.0.20240914-py3-none-any.whl", hash = "sha256:59c2f673eb55f32a99b2894faf6020e1a9f4a402ad0f192bfee0b64469054310"},
]
[package.dependencies]
@ -6733,13 +6730,13 @@ files = [
[[package]]
name = "virtualenv"
version = "20.27.0"
version = "20.26.6"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.27.0-py3-none-any.whl", hash = "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655"},
{file = "virtualenv-20.27.0.tar.gz", hash = "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2"},
{file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"},
{file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"},
]
[package.dependencies]
@ -7119,93 +7116,109 @@ requests = {version = "*", extras = ["socks"]}
[[package]]
name = "yarl"
version = "1.15.4"
version = "1.15.2"
description = "Yet another URL library"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.8"
files = [
{file = "yarl-1.15.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:551205388d1da18a9975302c9a274ba24788f53bb9bb86187496ebf9e938916e"},
{file = "yarl-1.15.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eee724176b5bc50ee64905f559345448119b860a30b9489bd7a073f61baf925f"},
{file = "yarl-1.15.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db818e33599f7b2e4c6507f2b2c24f45ff539a1b6e4e09163bb6f3cfb4616ca7"},
{file = "yarl-1.15.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07019a9de859c5a29916defd1e8c7557de6491a10bf50c49ff5284e6aedf5313"},
{file = "yarl-1.15.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db64a20e78969fc66665d2e5fc96cb4f4dc80f2137d8fed4b5a650ad569bb60f"},
{file = "yarl-1.15.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4076bfd8f1621449b19b9826848ed51bf0f2d1d38e82647c312c0730d8778903"},
{file = "yarl-1.15.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c23a442973dba3646811c284fce3dddd7fe5c2bd674ac73a122198e8218d6115"},
{file = "yarl-1.15.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2bdb038b3f5c284e3919218c580dedc95f592c417a358361450b9519b22f7a8"},
{file = "yarl-1.15.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:59db8e6888d5302b8dbca0c1026ddabe99d81d67cdc101941519e13ffc9050fe"},
{file = "yarl-1.15.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f3294ce265011547630a59c20085fcb6af8cc5fa1fa44a203251f7d86cd5d913"},
{file = "yarl-1.15.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4851618679ca70b863ba2e7109be5f09f8fd7715ec505bd42e5a947dcfde3a45"},
{file = "yarl-1.15.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:dce1c56beef74d9c799a6ed94001693232a1402138292353a8ce302b64f457d9"},
{file = "yarl-1.15.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1e7468f31de61a82817f918743e5229fce774f73fad58487cdf88eef4f06d864"},
{file = "yarl-1.15.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:527c68f48a91d953691291d3bce0209293aa5ad13ff05286ddb506791c331818"},
{file = "yarl-1.15.4-cp310-cp310-win32.whl", hash = "sha256:c30115cecaf25fdcb67cc71c669d08425207f62d7a2f6d5416057c1460529216"},
{file = "yarl-1.15.4-cp310-cp310-win_amd64.whl", hash = "sha256:df09c80f4bc2bc2efde309af383c3fe8fd8c51fe0519edb350b9c9e0af43ffa4"},
{file = "yarl-1.15.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:76259901cf1ac3db65e7e6dff04775b626d0715f9b51d92b447351144c756a82"},
{file = "yarl-1.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98d8dc1e8133f86d916125deca9780d791b22645f0d62bafe1452d1cd5eac631"},
{file = "yarl-1.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d0f16c87c62b7a94b389ddf6a8c9d081265d788875c39f3a80108c4856eea7b"},
{file = "yarl-1.15.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8de5328d91859b461899497980d4cc8269e84e2d18640f6ac643886fda9000bf"},
{file = "yarl-1.15.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84937d00e2ea03616c40977de20189fa13a9213e5744a3c6afa0e7dd9141d69c"},
{file = "yarl-1.15.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691a3b498fdebef63308e8967bb598cfd326c56d628da82b799dd181bace4503"},
{file = "yarl-1.15.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a706db0c3b7e4578ff34ed2b1d2507b08fd491346ffc64468786fdf1151d938"},
{file = "yarl-1.15.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:adb6b5d07d17c32f9d34c9dd4a693637a72323cfcb1f8a52d57033ab2dd21e99"},
{file = "yarl-1.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e100c6c7d9e9d469009fd55cc4d7ad168d67d40758865c50da713f7ada491e5"},
{file = "yarl-1.15.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6b254e55c8ac2362afaa651e3e53453aa19a095570792346245773b434176e"},
{file = "yarl-1.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8721f8bedaa722c3c483cc06a1399cbfdb280eadf443aa5d324b0203cef2a75f"},
{file = "yarl-1.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1005921b30f4f39bf893946df6173567ff650307babb5ec04bbf64342a1f62c1"},
{file = "yarl-1.15.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ab79cc13307065a0b3ef087f09f0509996fc605d35d6642bb28e5d85b2648e1e"},
{file = "yarl-1.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f337486742c700b102d640830aab3faf2848bed966b479a39e6783edd4ab1c6c"},
{file = "yarl-1.15.4-cp311-cp311-win32.whl", hash = "sha256:20acf84bd1ce530065f8e957e4a5878fda4bc5f18cb02659828210e1519de54e"},
{file = "yarl-1.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:ab9ccf26cb3fa32747ba2a637a189d2d42386a2fc4afc10dbc7f85922dd23b0f"},
{file = "yarl-1.15.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f923e94e93a37fd990e8336e0b9bedea533e7cbed14e0c572bf9357ef2a70681"},
{file = "yarl-1.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3198da7d7c34e29fc8c823e0c3ce6c7274aac35760de557c2017489c7d98fc5a"},
{file = "yarl-1.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d886de2ea81f513ba2d6820451d33b767a97c37867ba688d42e164b2dbca1362"},
{file = "yarl-1.15.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ac85e760543129a1912a82438fc8075223e35eaa2d457d61cd83c27d00d17be"},
{file = "yarl-1.15.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e58c5d07b1f78dd4cb180c5b3b82465cd281aaeee8aafea0e5d72a4b97922cb1"},
{file = "yarl-1.15.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9060589d0acad1fca048861fa9ee3e8ed060f67894fa885969648ab6e9e99a54"},
{file = "yarl-1.15.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccd6774aa7bebdf9ca608bb0839318757a71b8e0d2cf7b10c002bc8790bd343e"},
{file = "yarl-1.15.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7694f109867ee428c21b85ae19fd31d164c691eb45cc95c561cfdeba237a12e3"},
{file = "yarl-1.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:83e7154aa0d17f5c93d27ac01088fd9ab6673e7bab1acbd07cd7a865b980c045"},
{file = "yarl-1.15.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f16d1940c0cbc342f1d29d6212a006d172be616d2942c5c41966e8a3ce4c3be1"},
{file = "yarl-1.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7d5226c70af3ad9569ccc4ccc04ab65be79eeb22c87d7ae789c89e62ef76bbd6"},
{file = "yarl-1.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f25906e4a72d9833e81717c39a39dee7297ff5cb44957d06d177a2ab8ef2ef7f"},
{file = "yarl-1.15.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e07e4b17b648c880e8e42bf1ac0a730bde114961646ae1c2ec4433f0c11ca94"},
{file = "yarl-1.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f8136bde8dfa4477c6a85c79a366581b4a505b51a52b669318fb631d3f4f638"},
{file = "yarl-1.15.4-cp312-cp312-win32.whl", hash = "sha256:ccbeaf5b18b173b9d78e332e017b30ba8bedcf03cdce1d13490b82a3f421bc98"},
{file = "yarl-1.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:f74f6ffdc633aefecbc80282242a5395058db9d1247fa7dd2f070ef84dc82583"},
{file = "yarl-1.15.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4f66a0eda48844508736e47ed476d8fdd7cdbf16a4053b5d439509a25f708504"},
{file = "yarl-1.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fd2bb86f40962d53a91def15a2f7684c62e081a7b96ec74ed0259c34b15973b9"},
{file = "yarl-1.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f864b412557e69a6b953d62c01a0ed0ee342666298aa7f2a29af526bfa80f6e9"},
{file = "yarl-1.15.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a79c0a8bbb046add85663af85e9993b691bf20c2a109518bd35e0ce77edfe42"},
{file = "yarl-1.15.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de479e30abd2dfd49fdad3bd6953f2d930a45380be5143c0c9f7a1215cffc8cc"},
{file = "yarl-1.15.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21fabe58042f3e567b4edc75b2cf44cea02f228e41ac09d73de126bf685fe883"},
{file = "yarl-1.15.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77390496f2f32437a721c854897f889abefae0f3009daf90a2f703508d96c920"},
{file = "yarl-1.15.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3896bf15284dd23acab1f2e7fceb350d8da6f6f2436b922f7ec6b3de685d34ca"},
{file = "yarl-1.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:590e2d733a82ecf004c5c531cbef0d6be328e93adec960024eb213f10cb9503e"},
{file = "yarl-1.15.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1ceb677fb583971351627eac70eec6763fbc889761828da7a276681b5e39742d"},
{file = "yarl-1.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:69f628d2da1489b27959f4d63fdb326781fe484944dce94abbf919e416c54abe"},
{file = "yarl-1.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:35a6b69cc44bda002705d6138346bf0a0234cbb7c26c3bf192513eb946aee6f9"},
{file = "yarl-1.15.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:49f886e8dcf591275c6e20915b516fd81647857566b0c0158c52df1e468849c9"},
{file = "yarl-1.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:49190eb2ece70313742b0ea51520340288a059674da1f39eefb589d598d9453e"},
{file = "yarl-1.15.4-cp313-cp313-win32.whl", hash = "sha256:48334a6c8afee93097eb17c0a094234dac2d88da076c8cf372e09e2a5dcc4b66"},
{file = "yarl-1.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:f68025d6ba1816428b7de615c80f61cb03d5b7061158d4ced7696657a64aa59c"},
{file = "yarl-1.15.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8b569f4f511b59518ba6719feb5b8bf0a5d4115e6ac903c89e10a8a9ac656017"},
{file = "yarl-1.15.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9fe17744d60fc404ac61f824118e1e15ce3c2e92eced9b8e22f3c7847acafbf2"},
{file = "yarl-1.15.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:115346433fad2084ee3a1a925ccc0659990aa42e208ca54c278830a150a3caf3"},
{file = "yarl-1.15.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60165b8bc260f453321004b193770a66cc1b1a5c57c07d4b8dcc96839e7ad578"},
{file = "yarl-1.15.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65a0168691373e08d869d48b62c8bed0af0cdaef19c76e11ad73b43901bbdb5a"},
{file = "yarl-1.15.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:787532f00543a21b8f4ec3050b4e01b8fe437797903c0156a0b03dfca5e1ba6c"},
{file = "yarl-1.15.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f51c9d173e5fa4b12d06ddca09a41cabbdeb660471dbe55432423eec095709ab"},
{file = "yarl-1.15.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c96eaa30030e1cfafe533f3da8983812281235b7c50ef2a6c78ceca7aea1a0b"},
{file = "yarl-1.15.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4feab2dcb725eb5b4835207ecf3d370ff7ce930b253cba5e681646cb80d64c2c"},
{file = "yarl-1.15.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:de38b0b5b86e57efb129d179854e78b65cb8e294a8c75560877869c43aa2415a"},
{file = "yarl-1.15.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:65e0467f90f2acf3bc83bbfeedece8f1fd84df8add1a54e9600ed7b7b5debdb0"},
{file = "yarl-1.15.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:40c18f96696549e73b92dc12619f07019cbf5faefc1612608f967c144816e493"},
{file = "yarl-1.15.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:46491b3e058de7b484e1c9fb20aa8441f06d6c9a18395d711c1c2a9ad6707d6a"},
{file = "yarl-1.15.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:faa3dd7f4620ab5e5da7a0789d0aac78a9ad0376f102409d442ec5a4179e200a"},
{file = "yarl-1.15.4-cp39-cp39-win32.whl", hash = "sha256:c33ea7c55a73be343f02361795caf52a187357ea07708fb1cae6661ee1d689c8"},
{file = "yarl-1.15.4-cp39-cp39-win_amd64.whl", hash = "sha256:11b207061f28b4b6d980239b22ab0ecfadc47846b5a3b8e79f27fcc019d02cf9"},
{file = "yarl-1.15.4-py3-none-any.whl", hash = "sha256:e5cc288111c450c0a54a74475591b206d3b1cb47dc71bb6200f6be8b1337184c"},
{file = "yarl-1.15.4.tar.gz", hash = "sha256:a0c5e271058d148d730219ca4f33c5d841c6bd46e05b0da60fea7b516906ccd3"},
{file = "yarl-1.15.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e4ee8b8639070ff246ad3649294336b06db37a94bdea0d09ea491603e0be73b8"},
{file = "yarl-1.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a7cf963a357c5f00cb55b1955df8bbe68d2f2f65de065160a1c26b85a1e44172"},
{file = "yarl-1.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:43ebdcc120e2ca679dba01a779333a8ea76b50547b55e812b8b92818d604662c"},
{file = "yarl-1.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3433da95b51a75692dcf6cc8117a31410447c75a9a8187888f02ad45c0a86c50"},
{file = "yarl-1.15.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38d0124fa992dbacd0c48b1b755d3ee0a9f924f427f95b0ef376556a24debf01"},
{file = "yarl-1.15.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ded1b1803151dd0f20a8945508786d57c2f97a50289b16f2629f85433e546d47"},
{file = "yarl-1.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace4cad790f3bf872c082366c9edd7f8f8f77afe3992b134cfc810332206884f"},
{file = "yarl-1.15.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c77494a2f2282d9bbbbcab7c227a4d1b4bb829875c96251f66fb5f3bae4fb053"},
{file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b7f227ca6db5a9fda0a2b935a2ea34a7267589ffc63c8045f0e4edb8d8dcf956"},
{file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:31561a5b4d8dbef1559b3600b045607cf804bae040f64b5f5bca77da38084a8a"},
{file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3e52474256a7db9dcf3c5f4ca0b300fdea6c21cca0148c8891d03a025649d935"},
{file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1af74a9529a1137c67c887ed9cde62cff53aa4d84a3adbec329f9ec47a3936"},
{file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:15c87339490100c63472a76d87fe7097a0835c705eb5ae79fd96e343473629ed"},
{file = "yarl-1.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:74abb8709ea54cc483c4fb57fb17bb66f8e0f04438cff6ded322074dbd17c7ec"},
{file = "yarl-1.15.2-cp310-cp310-win32.whl", hash = "sha256:ffd591e22b22f9cb48e472529db6a47203c41c2c5911ff0a52e85723196c0d75"},
{file = "yarl-1.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:1695497bb2a02a6de60064c9f077a4ae9c25c73624e0d43e3aa9d16d983073c2"},
{file = "yarl-1.15.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9fcda20b2de7042cc35cf911702fa3d8311bd40055a14446c1e62403684afdc5"},
{file = "yarl-1.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0545de8c688fbbf3088f9e8b801157923be4bf8e7b03e97c2ecd4dfa39e48e0e"},
{file = "yarl-1.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbda058a9a68bec347962595f50546a8a4a34fd7b0654a7b9697917dc2bf810d"},
{file = "yarl-1.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ac2bc069f4a458634c26b101c2341b18da85cb96afe0015990507efec2e417"},
{file = "yarl-1.15.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd126498171f752dd85737ab1544329a4520c53eed3997f9b08aefbafb1cc53b"},
{file = "yarl-1.15.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3db817b4e95eb05c362e3b45dafe7144b18603e1211f4a5b36eb9522ecc62bcf"},
{file = "yarl-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:076b1ed2ac819933895b1a000904f62d615fe4533a5cf3e052ff9a1da560575c"},
{file = "yarl-1.15.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f8cfd847e6b9ecf9f2f2531c8427035f291ec286c0a4944b0a9fce58c6446046"},
{file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:32b66be100ac5739065496c74c4b7f3015cef792c3174982809274d7e51b3e04"},
{file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34a2d76a1984cac04ff8b1bfc939ec9dc0914821264d4a9c8fd0ed6aa8d4cfd2"},
{file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0afad2cd484908f472c8fe2e8ef499facee54a0a6978be0e0cff67b1254fd747"},
{file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c68e820879ff39992c7f148113b46efcd6ec765a4865581f2902b3c43a5f4bbb"},
{file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:98f68df80ec6ca3015186b2677c208c096d646ef37bbf8b49764ab4a38183931"},
{file = "yarl-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56ec1eacd0a5d35b8a29f468659c47f4fe61b2cab948ca756c39b7617f0aa5"},
{file = "yarl-1.15.2-cp311-cp311-win32.whl", hash = "sha256:eedc3f247ee7b3808ea07205f3e7d7879bc19ad3e6222195cd5fbf9988853e4d"},
{file = "yarl-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:0ccaa1bc98751fbfcf53dc8dfdb90d96e98838010fc254180dd6707a6e8bb179"},
{file = "yarl-1.15.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82d5161e8cb8f36ec778fd7ac4d740415d84030f5b9ef8fe4da54784a1f46c94"},
{file = "yarl-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa2bea05ff0a8fb4d8124498e00e02398f06d23cdadd0fe027d84a3f7afde31e"},
{file = "yarl-1.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99e12d2bf587b44deb74e0d6170fec37adb489964dbca656ec41a7cd8f2ff178"},
{file = "yarl-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:243fbbbf003754fe41b5bdf10ce1e7f80bcc70732b5b54222c124d6b4c2ab31c"},
{file = "yarl-1.15.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:856b7f1a7b98a8c31823285786bd566cf06226ac4f38b3ef462f593c608a9bd6"},
{file = "yarl-1.15.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:553dad9af802a9ad1a6525e7528152a015b85fb8dbf764ebfc755c695f488367"},
{file = "yarl-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30c3ff305f6e06650a761c4393666f77384f1cc6c5c0251965d6bfa5fbc88f7f"},
{file = "yarl-1.15.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:353665775be69bbfc6d54c8d134bfc533e332149faeddd631b0bc79df0897f46"},
{file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f4fe99ce44128c71233d0d72152db31ca119711dfc5f2c82385ad611d8d7f897"},
{file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9c1e3ff4b89cdd2e1a24c214f141e848b9e0451f08d7d4963cb4108d4d798f1f"},
{file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:711bdfae4e699a6d4f371137cbe9e740dc958530cb920eb6f43ff9551e17cfbc"},
{file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4388c72174868884f76affcdd3656544c426407e0043c89b684d22fb265e04a5"},
{file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0e1844ad47c7bd5d6fa784f1d4accc5f4168b48999303a868fe0f8597bde715"},
{file = "yarl-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5cafb02cf097a82d74403f7e0b6b9df3ffbfe8edf9415ea816314711764a27b"},
{file = "yarl-1.15.2-cp312-cp312-win32.whl", hash = "sha256:156ececdf636143f508770bf8a3a0498de64da5abd890c7dbb42ca9e3b6c05b8"},
{file = "yarl-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:435aca062444a7f0c884861d2e3ea79883bd1cd19d0a381928b69ae1b85bc51d"},
{file = "yarl-1.15.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:416f2e3beaeae81e2f7a45dc711258be5bdc79c940a9a270b266c0bec038fb84"},
{file = "yarl-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:173563f3696124372831007e3d4b9821746964a95968628f7075d9231ac6bb33"},
{file = "yarl-1.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ce2e0f6123a60bd1a7f5ae3b2c49b240c12c132847f17aa990b841a417598a2"},
{file = "yarl-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaea112aed589131f73d50d570a6864728bd7c0c66ef6c9154ed7b59f24da611"},
{file = "yarl-1.15.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4ca3b9f370f218cc2a0309542cab8d0acdfd66667e7c37d04d617012485f904"},
{file = "yarl-1.15.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23ec1d3c31882b2a8a69c801ef58ebf7bae2553211ebbddf04235be275a38548"},
{file = "yarl-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75119badf45f7183e10e348edff5a76a94dc19ba9287d94001ff05e81475967b"},
{file = "yarl-1.15.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e6fdc976ec966b99e4daa3812fac0274cc28cd2b24b0d92462e2e5ef90d368"},
{file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8657d3f37f781d987037f9cc20bbc8b40425fa14380c87da0cb8dfce7c92d0fb"},
{file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:93bed8a8084544c6efe8856c362af08a23e959340c87a95687fdbe9c9f280c8b"},
{file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:69d5856d526802cbda768d3e6246cd0d77450fa2a4bc2ea0ea14f0d972c2894b"},
{file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ccad2800dfdff34392448c4bf834be124f10a5bc102f254521d931c1c53c455a"},
{file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a880372e2e5dbb9258a4e8ff43f13888039abb9dd6d515f28611c54361bc5644"},
{file = "yarl-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c998d0558805860503bc3a595994895ca0f7835e00668dadc673bbf7f5fbfcbe"},
{file = "yarl-1.15.2-cp313-cp313-win32.whl", hash = "sha256:533a28754e7f7439f217550a497bb026c54072dbe16402b183fdbca2431935a9"},
{file = "yarl-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:5838f2b79dc8f96fdc44077c9e4e2e33d7089b10788464609df788eb97d03aad"},
{file = "yarl-1.15.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fbbb63bed5fcd70cd3dd23a087cd78e4675fb5a2963b8af53f945cbbca79ae16"},
{file = "yarl-1.15.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2e93b88ecc8f74074012e18d679fb2e9c746f2a56f79cd5e2b1afcf2a8a786b"},
{file = "yarl-1.15.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af8ff8d7dc07ce873f643de6dfbcd45dc3db2c87462e5c387267197f59e6d776"},
{file = "yarl-1.15.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66f629632220a4e7858b58e4857927dd01a850a4cef2fb4044c8662787165cf7"},
{file = "yarl-1.15.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:833547179c31f9bec39b49601d282d6f0ea1633620701288934c5f66d88c3e50"},
{file = "yarl-1.15.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2aa738e0282be54eede1e3f36b81f1e46aee7ec7602aa563e81e0e8d7b67963f"},
{file = "yarl-1.15.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a13a07532e8e1c4a5a3afff0ca4553da23409fad65def1b71186fb867eeae8d"},
{file = "yarl-1.15.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c45817e3e6972109d1a2c65091504a537e257bc3c885b4e78a95baa96df6a3f8"},
{file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:670eb11325ed3a6209339974b276811867defe52f4188fe18dc49855774fa9cf"},
{file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:d417a4f6943112fae3924bae2af7112562285848d9bcee737fc4ff7cbd450e6c"},
{file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bc8936d06cd53fddd4892677d65e98af514c8d78c79864f418bbf78a4a2edde4"},
{file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:954dde77c404084c2544e572f342aef384240b3e434e06cecc71597e95fd1ce7"},
{file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5bc0df728e4def5e15a754521e8882ba5a5121bd6b5a3a0ff7efda5d6558ab3d"},
{file = "yarl-1.15.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b71862a652f50babab4a43a487f157d26b464b1dedbcc0afda02fd64f3809d04"},
{file = "yarl-1.15.2-cp38-cp38-win32.whl", hash = "sha256:63eab904f8630aed5a68f2d0aeab565dcfc595dc1bf0b91b71d9ddd43dea3aea"},
{file = "yarl-1.15.2-cp38-cp38-win_amd64.whl", hash = "sha256:2cf441c4b6e538ba0d2591574f95d3fdd33f1efafa864faa077d9636ecc0c4e9"},
{file = "yarl-1.15.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a32d58f4b521bb98b2c0aa9da407f8bd57ca81f34362bcb090e4a79e9924fefc"},
{file = "yarl-1.15.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:766dcc00b943c089349d4060b935c76281f6be225e39994c2ccec3a2a36ad627"},
{file = "yarl-1.15.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bed1b5dbf90bad3bfc19439258c97873eab453c71d8b6869c136346acfe497e7"},
{file = "yarl-1.15.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed20a4bdc635f36cb19e630bfc644181dd075839b6fc84cac51c0f381ac472e2"},
{file = "yarl-1.15.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d538df442c0d9665664ab6dd5fccd0110fa3b364914f9c85b3ef9b7b2e157980"},
{file = "yarl-1.15.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c6cf1d92edf936ceedc7afa61b07e9d78a27b15244aa46bbcd534c7458ee1b"},
{file = "yarl-1.15.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce44217ad99ffad8027d2fde0269ae368c86db66ea0571c62a000798d69401fb"},
{file = "yarl-1.15.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47a6000a7e833ebfe5886b56a31cb2ff12120b1efd4578a6fcc38df16cc77bd"},
{file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e52f77a0cd246086afde8815039f3e16f8d2be51786c0a39b57104c563c5cbb0"},
{file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:f9ca0e6ce7774dc7830dc0cc4bb6b3eec769db667f230e7c770a628c1aa5681b"},
{file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:136f9db0f53c0206db38b8cd0c985c78ded5fd596c9a86ce5c0b92afb91c3a19"},
{file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:173866d9f7409c0fb514cf6e78952e65816600cb888c68b37b41147349fe0057"},
{file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:6e840553c9c494a35e449a987ca2c4f8372668ee954a03a9a9685075228e5036"},
{file = "yarl-1.15.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:458c0c65802d816a6b955cf3603186de79e8fdb46d4f19abaec4ef0a906f50a7"},
{file = "yarl-1.15.2-cp39-cp39-win32.whl", hash = "sha256:5b48388ded01f6f2429a8c55012bdbd1c2a0c3735b3e73e221649e524c34a58d"},
{file = "yarl-1.15.2-cp39-cp39-win_amd64.whl", hash = "sha256:81dadafb3aa124f86dc267a2168f71bbd2bfb163663661ab0038f6e4b8edb810"},
{file = "yarl-1.15.2-py3-none-any.whl", hash = "sha256:0d3105efab7c5c091609abacad33afff33bdff0035bece164c98bcf5a85ef90a"},
{file = "yarl-1.15.2.tar.gz", hash = "sha256:a39c36f4218a5bb668b4f06874d676d35a035ee668e6e7e3538835c703634b84"},
]
[package.dependencies]