From bb61d144c5a7bfd545296d01f908e154e2dadf24 Mon Sep 17 00:00:00 2001 From: Alexander-D-Karpov Date: Mon, 10 Jul 2023 03:13:42 +0300 Subject: [PATCH] fixed music load, added radio --- akarpov/common/channels.py | 11 + .../models/web => music/api}/__init__.py | 0 akarpov/music/api/serializers.py | 44 +++ akarpov/music/apps.py | 10 + akarpov/music/consumers.py | 51 +++ akarpov/music/migrations/0004_radiosong.py | 37 ++ .../music/migrations/0005_radiosong_start.py | 18 + akarpov/music/models.py | 6 + akarpov/music/services/db.py | 58 ++- akarpov/music/services/file.py | 75 ++-- akarpov/music/services/youtube.py | 49 +-- akarpov/music/tasks.py | 41 +++ akarpov/music/urls.py | 1 + akarpov/music/views.py | 10 +- akarpov/templates/music/radio.html | 330 ++++++++++++++++++ akarpov/templates/music/upload.html | 20 ++ akarpov/utils/celery.py | 10 + akarpov/utils/channels.py | 14 +- config/asgi.py | 6 +- config/routing.py | 8 +- config/settings/base.py | 12 + poetry.lock | 243 ++++++++++++- pyproject.toml | 2 + 23 files changed, 952 insertions(+), 104 deletions(-) rename akarpov/{pipeliner/models/web => music/api}/__init__.py (100%) create mode 100644 akarpov/music/api/serializers.py create mode 100644 akarpov/music/consumers.py create mode 100644 akarpov/music/migrations/0004_radiosong.py create mode 100644 akarpov/music/migrations/0005_radiosong_start.py create mode 100644 akarpov/templates/music/radio.html create mode 100644 akarpov/templates/music/upload.html create mode 100644 akarpov/utils/celery.py diff --git a/akarpov/common/channels.py b/akarpov/common/channels.py index eddc16f..4d3c6e1 100644 --- a/akarpov/common/channels.py +++ b/akarpov/common/channels.py @@ -1,4 +1,5 @@ from channels.db import database_sync_to_async +from channels.generic.websocket import AsyncJsonWebsocketConsumer, JsonWebsocketConsumer from akarpov.common.jwt import read_jwt @@ -29,3 +30,13 @@ async def __call__(self, scope, receive, send): scope["user"] = await get_user(dict(scope["headers"])) return await self.app(scope, receive, send) + + +class BaseConsumer(AsyncJsonWebsocketConsumer): + async def send_error(self, msg): + await self.send_json({"type": "error", "data": {"msg": msg}}) + + +class SyncBaseConsumer(JsonWebsocketConsumer): + def send_error(self, msg): + self.send_json({"type": "error", "data": {"msg": msg}}) diff --git a/akarpov/pipeliner/models/web/__init__.py b/akarpov/music/api/__init__.py similarity index 100% rename from akarpov/pipeliner/models/web/__init__.py rename to akarpov/music/api/__init__.py diff --git a/akarpov/music/api/serializers.py b/akarpov/music/api/serializers.py new file mode 100644 index 0000000..4100c9a --- /dev/null +++ b/akarpov/music/api/serializers.py @@ -0,0 +1,44 @@ +from rest_framework import serializers + +from akarpov.music.models import Album, Author, Song + + +class AuthorSerializer(serializers.ModelSerializer): + url = serializers.SerializerMethodField(method_name="get_url") + + def get_url(self, obj): + return obj.get_absolute_url() + + class Meta: + model = Author + fields = ["name", "link", "image_cropped", "url"] + + +class AlbumSerializer(serializers.ModelSerializer): + url = serializers.SerializerMethodField(method_name="get_url") + + def get_url(self, obj): + return obj.get_absolute_url() + + class Meta: + model = Album + fields = ["name", "link", "image_cropped", "url"] + + +class SongSerializer(serializers.ModelSerializer): + authors = AuthorSerializer(many=True) + album = AlbumSerializer() + + class Meta: + model = Song + fields = [ + "id", + "image", + "link", + "length", + "played", + "name", + "file", + "authors", + "album", + ] diff --git a/akarpov/music/apps.py b/akarpov/music/apps.py index bdd37e7..f2444a5 100644 --- a/akarpov/music/apps.py +++ b/akarpov/music/apps.py @@ -1,4 +1,5 @@ from django.apps import AppConfig +from django.core.exceptions import AppRegistryNotReady class MusicConfig(AppConfig): @@ -10,3 +11,12 @@ def ready(self): import akarpov.music.signals # noqa F401 except ImportError: pass + try: + from akarpov.music.tasks import start_next_song + + start_next_song.apply_async( + kwargs={"previous_ids": []}, + countdown=5, + ) + except AppRegistryNotReady: + pass diff --git a/akarpov/music/consumers.py b/akarpov/music/consumers.py new file mode 100644 index 0000000..ac84c1a --- /dev/null +++ b/akarpov/music/consumers.py @@ -0,0 +1,51 @@ +import json + +from channels.db import database_sync_to_async +from django.utils.timezone import now + +from akarpov.common.channels import BaseConsumer +from akarpov.music.api.serializers import SongSerializer +from akarpov.music.models import RadioSong + + +class RadioConsumer(BaseConsumer): + def __init__(self, *args, **kwargs): + super().__init__(args, kwargs) + self.room_group_name = None + + async def connect(self): + self.room_group_name = "radio_main" + + await self.accept() + await self.channel_layer.group_add(self.room_group_name, self.channel_name) + data = await self.get_radio_song() + if data: + await self.send_json(data) + + @database_sync_to_async + def get_radio_song(self): + r = RadioSong.objects.filter(slug="") + if r: + r = r.first() + return SongSerializer(context={"request": None}).to_representation( + r.song + ) | {"offset": (now() - r.start).seconds} + return None + + async def disconnect(self, close_code): + await self.channel_layer.group_discard(self.room_group_name, self.channel_name) + + async def receive(self, text_data): + data = None + + try: + data = json.loads(text_data) + except ValueError: + await self.send_json( + {"type": "ERROR", "message": "data is not JSON serializable"} + ) + return data + + async def song(self, event): + data = event["data"] + await self.send_json(data) diff --git a/akarpov/music/migrations/0004_radiosong.py b/akarpov/music/migrations/0004_radiosong.py new file mode 100644 index 0000000..c4b85eb --- /dev/null +++ b/akarpov/music/migrations/0004_radiosong.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.3 on 2023-07-09 19:20 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("music", "0003_remove_song_author_remove_songinque_song_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="RadioSong", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slug", models.SlugField(unique=True)), + ( + "song", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="radio", + to="music.song", + ), + ), + ], + ), + ] diff --git a/akarpov/music/migrations/0005_radiosong_start.py b/akarpov/music/migrations/0005_radiosong_start.py new file mode 100644 index 0000000..0d0a6eb --- /dev/null +++ b/akarpov/music/migrations/0005_radiosong_start.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-09 19:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("music", "0004_radiosong"), + ] + + operations = [ + migrations.AddField( + model_name="radiosong", + name="start", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/akarpov/music/models.py b/akarpov/music/models.py index 1016124..6a92a93 100644 --- a/akarpov/music/models.py +++ b/akarpov/music/models.py @@ -83,3 +83,9 @@ class SongInQue(models.Model): name = models.CharField(blank=True, max_length=250) status = models.CharField(null=True, blank=True, max_length=250) error = models.BooleanField(default=False) + + +class RadioSong(models.Model): + start = models.DateTimeField(auto_now=True) + slug = models.SlugField(unique=True) + song = models.ForeignKey("Song", related_name="radio", on_delete=models.CASCADE) diff --git a/akarpov/music/services/db.py b/akarpov/music/services/db.py index 3156362..b5ef3ad 100644 --- a/akarpov/music/services/db.py +++ b/akarpov/music/services/db.py @@ -1,6 +1,8 @@ import os from django.core.files import File +from mutagen import File as MutagenFile +from mutagen.id3 import APIC, ID3, TCON, TORY, TextFrame from mutagen.mp3 import MP3 from PIL import Image from pydub import AudioSegment @@ -15,10 +17,11 @@ def load_track( album: str | None = None, name: str | None = None, link: str | None = None, + **kwargs, ) -> Song: p_name = path.split("/")[-1] if authors: - authors = [Author.objects.get_or_create(name=x)[0] for x in authors] + authors = [Author.objects.get_or_create(name=x)[0] for x in authors if authors] else: authors = [] if album: @@ -27,7 +30,9 @@ def load_track( album = None if sng := Song.objects.filter( - name=name if name else p_name, authors=authors, album=album + name=name if name else p_name, + authors__id__in=[x.id for x in authors], + album=album, ): return sng.first() @@ -37,8 +42,7 @@ def load_track( os.remove(path) path = mp3_path - audio = MP3(path) - + tag = MP3(path, ID3=ID3) if image_path: if not image_path.endswith(".png"): im = Image.open(image_path) @@ -46,18 +50,50 @@ def load_track( im.save(image_path) song = Song( - link=link, length=audio.info.length, name=name if name else p_name, album=album + link=link if link else "", + length=tag.info.length, + name=name if name else p_name, + album=album, ) - with open(path, "rb") as file: - song.file = File(file, name=path.split("/")[-1]) - if image_path: - with open(image_path, "rb") as file: - song.image = File(file, name=image_path.split("/")[-1]) + with open(path, "rb") as file, open(image_path, "rb") as image: + song.image = File(image, name=image_path.split("/")[-1]) + song.file = File(file, name=path.split("/")[-1]) + song.save() + else: + with open(path, "rb") as file: + song.file = File(file, name=path.split("/")[-1]) + song.save() if authors: song.authors.set(authors) - song.save() + # set music meta + tag = MutagenFile(song.file.path) + tag["title"] = TextFrame(encoding=3, text=[name]) + if album: + tag["album"] = TextFrame(encoding=3, text=[album.name]) + if authors: + tag["artist"] = TextFrame(encoding=3, text=[x.name for x in authors]) + tag.save() + + tag = MP3(song.file.path, ID3=ID3) + if image_path: + with open(image_path, "rb") as f: + tag.tags.add( + APIC( + encoding=3, # 3 is for utf-8 + mime="image/png", # image/jpeg or image/png + type=3, # 3 is for the cover image + desc="Cover", + data=f.read(), + ) + ) + if "release" in kwargs: + tag.tags.add(TORY(text=kwargs["release"])) + if "genre" in kwargs: + tag.tags.add(TCON(text=kwargs["genre"])) + tag.save() + return song diff --git a/akarpov/music/services/file.py b/akarpov/music/services/file.py index cec7a19..a4c28f6 100644 --- a/akarpov/music/services/file.py +++ b/akarpov/music/services/file.py @@ -1,49 +1,60 @@ +import os +from io import BytesIO from pathlib import Path +from random import randint import mutagen -from django.core.files import File +from mutagen.id3 import ID3 +from PIL import Image, UnidentifiedImageError -from akarpov.music.models import Album, Author, Song, SongInQue +from akarpov.music.models import Song +from akarpov.music.services.db import load_track def load_dir(path: str): path = Path(path) for f in list(path.glob("**/*.mp3")): - with f.open("rb") as file: - process_mp3_file(File(file, name=str(f).split("/")[-1]), str(f)) + process_mp3_file(str(f)) def load_file(path: str): - with open(path, "rb") as file: - process_mp3_file(File(file, name=path.split("/")[-1]), path) + process_mp3_file(path) -def process_mp3_file(file: File, path: str) -> None: - que = SongInQue.objects.create() - try: - tag = mutagen.File(path, easy=True) - que.name = tag["title"][0] if "title" in tag else path.split("/")[-1] - que.save() - if "artist" in tag: - author = Author.objects.get_or_create(name=tag["artist"][0])[0] - else: - author = None +def process_mp3_file(path: str) -> None: + tag = mutagen.File(path, easy=True) + if "artist" in tag: + author = tag["artist"] + else: + author = None - if "album" in tag: - album = Album.objects.get_or_create(name=tag["album"][0])[0] - else: - album = None + if "album" in tag: + album = tag["album"] + else: + album = None + name = tag["title"][0] if "title" in tag else path.split("/")[-1] + f = Song.objects.filter(name=name) + if author: + f.filter(authors__name__in=author) + if album: + f.filter(album__name=album) + if f.exists(): + return - song, created = Song.objects.get_or_create( - name=tag["title"][0] if "title" in tag else path.split("/")[-1], - author=author, - album=album, - ) - song.file = file - song.save(update_fields=["file"]) - que.delete() - except Exception as e: - que.name = e - que.error = True - que.save() + tags = ID3(path) + pict = [x for x in tags.getall("APIC") if x] + image_pth = None + if pict: + try: + pict = pict[0].data + im = Image.open(BytesIO(pict)) + image_pth = f"/tmp/{randint(1, 1000000)}.png" + if os.path.exists(image_pth): + image_pth = f"/tmp/{randint(1, 1000000)}.png" + im.save(image_pth) + except UnidentifiedImageError: + pass + load_track(path, image_pth, author, album, name) + if image_pth and os.path.exists(image_pth): + os.remove(image_pth) diff --git a/akarpov/music/services/youtube.py b/akarpov/music/services/youtube.py index 7d8633e..7bf0c76 100644 --- a/akarpov/music/services/youtube.py +++ b/akarpov/music/services/youtube.py @@ -1,18 +1,14 @@ import os -from pathlib import Path from random import randint import requests from django.conf import settings -from django.core.files import File -from mutagen.easyid3 import EasyID3 -from mutagen.id3 import APIC, ID3, TCON, TORY -from mutagen.mp3 import MP3 from PIL import Image from pydub import AudioSegment from pytube import Search, YouTube -from akarpov.music.models import Album, Author, Song, SongInQue +from akarpov.music.models import Song, SongInQue +from akarpov.music.services.db import load_track from akarpov.music.services.spotify import get_track_info @@ -37,9 +33,6 @@ def download_from_youtube_link(link: str) -> Song: que.delete() return sng.first() - authors = [Author.objects.get_or_create(name=x)[0] for x in info["artists"]] - album = Album.objects.get_or_create(name=info["album_name"])[0] - audio = yt.streams.filter(only_audio=True).order_by("abr").desc().first() orig_path = audio.download(output_path=settings.MEDIA_ROOT) @@ -52,7 +45,7 @@ def download_from_youtube_link(link: str) -> Song: r = requests.get(info["album_image"]) img_pth = str( settings.MEDIA_ROOT - + f"/{info['album_image'].split('/')[-1]}_{str(randint(100, 999))}" + + f"/{info['album_image'].split('/')[-1]}_{str(randint(100, 999))}.png" ) with open(img_pth, "wb") as f: f.write(r.content) @@ -62,41 +55,9 @@ def download_from_youtube_link(link: str) -> Song: os.remove(img_pth) - # set music meta - tag = MP3(path, ID3=ID3) - tag.tags.add( - APIC( - encoding=3, # 3 is for utf-8 - mime="image/png", # image/jpeg or image/png - type=3, # 3 is for the cover image - desc="Cover", - data=open(str(f"{img_pth}.png"), "rb").read(), - ) - ) - tag.tags.add(TORY(text=info["release"])) - if "genre" in info: - tag.tags.add(TCON(text=info["genre"])) - - tag.save() - os.remove(str(f"{img_pth}.png")) - tag = EasyID3(path) - - tag["title"] = info["title"] - tag["album"] = info["album_name"] - tag["artist"] = info["artist"] - - tag.save() - - # save track - ms_path = Path(path) - song = Song(name=info["title"], author=authors[0], album=album) - with ms_path.open(mode="rb") as f: - song.file = File(f, name=ms_path.name) - song.save() - os.remove(path) - que.delete() - return song + load_track(path, img_pth, info["artists"], info["album_name"]) except Exception as e: + print(e) que.name = e que.error = True que.save() diff --git a/akarpov/music/tasks.py b/akarpov/music/tasks.py index 4d255cc..13194e6 100644 --- a/akarpov/music/tasks.py +++ b/akarpov/music/tasks.py @@ -1,8 +1,13 @@ +from asgiref.sync import async_to_sync from celery import shared_task +from channels.layers import get_channel_layer from pytube import Channel, Playlist +from akarpov.music.api.serializers import SongSerializer +from akarpov.music.models import RadioSong, Song from akarpov.music.services import yandex, youtube from akarpov.music.services.file import load_dir, load_file +from akarpov.utils.celery import get_scheduled_tasks_name @shared_task @@ -44,3 +49,39 @@ def process_file(path): @shared_task def load_ym_file_meta(track): return yandex.load_file_meta(track) + + +@shared_task() +def start_next_song(previous_ids: list): + f = Song.objects.filter(length__isnull=False).exclude(id__in=previous_ids) + if not f: + previous_ids = [] + f = Song.objects.filter(length__isnull=False) + if not f: + if "akarpov.music.tasks.start_next_song" not in get_scheduled_tasks_name(): + start_next_song.apply_async( + kwargs={"previous_ids": []}, + countdown=60, + ) + else: + song = f.order_by("?").first() + data = SongSerializer(context={"request": None}).to_representation(song) + channel_layer = get_channel_layer() + async_to_sync(channel_layer.group_send)( + "radio_main", {"type": "song", "data": data} + ) + song.played += 1 + song.save(update_fields=["played"]) + if RadioSong.objects.filter(slug="").exists(): + r = RadioSong.objects.get(slug="") + r.song = song + r.save() + else: + RadioSong.objects.create(song=song, slug="") + previous_ids.append(song.id) + if "akarpov.music.tasks.start_next_song" not in get_scheduled_tasks_name(): + start_next_song.apply_async( + kwargs={"previous_ids": previous_ids}, + countdown=song.length, + ) + return diff --git a/akarpov/music/urls.py b/akarpov/music/urls.py index 63790cd..a674d47 100644 --- a/akarpov/music/urls.py +++ b/akarpov/music/urls.py @@ -11,4 +11,5 @@ path("album/", views.album_view, name="album"), path("author/", views.author_view, name="author"), path("playlist/", views.playlist_view, name="playlist"), + path("radio/", views.radio_main_view, name="radio"), ] diff --git a/akarpov/music/views.py b/akarpov/music/views.py index db9d5f6..9926398 100644 --- a/akarpov/music/views.py +++ b/akarpov/music/views.py @@ -48,13 +48,14 @@ class PlaylistView(generic.DetailView): class LoadTrackView(SuperUserRequiredMixin, generic.FormView): form_class = TracksLoadForm + template_name = "music/upload.html" def get_success_url(self): # TODO: add room to see tracks load return "" def form_valid(self, form): - load_tracks(form.address) + load_tracks(form.data["address"]) return super().form_valid(form) @@ -76,3 +77,10 @@ def form_valid(self, form): load_track_file_view = LoadTrackFileView.as_view() + + +class MainRadioView(generic.TemplateView): + template_name = "music/radio.html" + + +radio_main_view = MainRadioView.as_view() diff --git a/akarpov/templates/music/radio.html b/akarpov/templates/music/radio.html new file mode 100644 index 0000000..02d4764 --- /dev/null +++ b/akarpov/templates/music/radio.html @@ -0,0 +1,330 @@ +{% extends 'base.html' %} + +{% block css %} + +{% endblock %} + +{% block content %} +
+
    +
  • +
  • +

    +

    +

    I Need You Back

    + +
    + +
    +

    0:00

    +
    + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
  • +
+
+{% endblock %} + +{% block inline_javascript %} + + +{% endblock %} diff --git a/akarpov/templates/music/upload.html b/akarpov/templates/music/upload.html new file mode 100644 index 0000000..da1a1e6 --- /dev/null +++ b/akarpov/templates/music/upload.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% load static %} +{% load crispy_forms_tags %} + +{% block title %}editing post on akarpov{% endblock %} + +{% block content %} +
+ {% csrf_token %} + {{ form.media }} + {% for field in form %} + {{ field|as_crispy_field }} + {% endfor %} +
+ +
+
+{% endblock %} diff --git a/akarpov/utils/celery.py b/akarpov/utils/celery.py new file mode 100644 index 0000000..761c5c3 --- /dev/null +++ b/akarpov/utils/celery.py @@ -0,0 +1,10 @@ +from config.celery_app import app + + +def get_scheduled_tasks_name() -> [str]: + i = app.control.inspect() + t = i.scheduled() + all_tasks = [] + for worker, tasks in t.items(): + all_tasks += tasks + return [x["request"]["name"] for x in all_tasks] diff --git a/akarpov/utils/channels.py b/akarpov/utils/channels.py index f6df84b..f2e6b09 100644 --- a/akarpov/utils/channels.py +++ b/akarpov/utils/channels.py @@ -1,24 +1,12 @@ import functools -from channels.generic.websocket import AsyncJsonWebsocketConsumer, JsonWebsocketConsumer - def login_required(func): @functools.wraps(func) def wrapper(self, *args, **kwargs): if not self.scope.get("user", False) or not self.scope["user"].is_authenticated: - self.send_error("Требуется авторизация") + self.send_error("Auth is required") else: return func(self, *args, **kwargs) return wrapper - - -class BaseConsumer(AsyncJsonWebsocketConsumer): - async def send_error(self, msg): - await self.send_json({"type": "error", "data": {"msg": msg}}) - - -class SyncBaseConsumer(JsonWebsocketConsumer): - def send_error(self, msg): - self.send_json({"type": "error", "data": {"msg": msg}}) diff --git a/config/asgi.py b/config/asgi.py index 2f439ec..eef501c 100644 --- a/config/asgi.py +++ b/config/asgi.py @@ -1,12 +1,16 @@ import os from channels.routing import ProtocolTypeRouter, URLRouter +from django.conf import settings from django.core.asgi import get_asgi_application from akarpov.common.channels import HeaderAuthMiddleware from config import routing -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") +if settings.DEBUG: + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") +else: + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") application = ProtocolTypeRouter( { diff --git a/config/routing.py b/config/routing.py index b3600a3..4cfa959 100644 --- a/config/routing.py +++ b/config/routing.py @@ -1 +1,7 @@ -websocket_urlpatterns = [] +from django.urls import re_path + +from akarpov.music.consumers import RadioConsumer + +websocket_urlpatterns = [ + re_path(r"ws/radio/", RadioConsumer.as_asgi()), +] diff --git a/config/settings/base.py b/config/settings/base.py index 39cf9dc..d7bf154 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -76,6 +76,18 @@ WSGI_APPLICATION = "config.wsgi.application" ASGI_APPLICATION = "config.asgi.application" + +# CHANNELS +# ------------------------------------------------------------------------------ +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [("127.0.0.1", 6379)], + }, + }, +} + # APPS # ------------------------------------------------------------------------------ DJANGO_APPS = [ diff --git a/poetry.lock b/poetry.lock index dd284b1..9d1065b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -180,6 +180,18 @@ imageio = ">=1.5" numpy = ">=1.11.1" Pillow = ">=3.3.1" +[[package]] +name = "annotated-types" +version = "0.5.0" +description = "Reusable constraint types to use with typing.Annotated" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"}, + {file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"}, +] + [[package]] name = "anyio" version = "3.7.1" @@ -726,6 +738,28 @@ Django = ">=3.2" daphne = ["daphne (>=4.0.0)"] tests = ["async-timeout", "coverage (>=4.5,<5.0)", "pytest", "pytest-asyncio", "pytest-django"] +[[package]] +name = "channels-redis" +version = "4.1.0" +description = "Redis-backed ASGI channel layer implementation" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "channels_redis-4.1.0-py3-none-any.whl", hash = "sha256:3696f5b9fe367ea495d402ba83d7c3c99e8ca0e1354ff8d913535976ed0abf73"}, + {file = "channels_redis-4.1.0.tar.gz", hash = "sha256:6bd4f75f4ab4a7db17cee495593ace886d7e914c66f8214a1f247ff6659c073a"}, +] + +[package.dependencies] +asgiref = ">=3.2.10,<4" +channels = "*" +msgpack = ">=1.0,<2.0" +redis = ">=4.5.3" + +[package.extras] +cryptography = ["cryptography (>=1.3.0)"] +tests = ["async-timeout", "cryptography (>=1.3.0)", "pytest", "pytest-asyncio", "pytest-timeout"] + [[package]] name = "chardet" version = "3.0.4" @@ -3076,6 +3110,79 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "msgpack" +version = "1.0.5" +description = "MessagePack serializer" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, + {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, + {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, + {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, + {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"}, + {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"}, + {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"}, + {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"}, + {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"}, + {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"}, + {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"}, + {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"}, + {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"}, + {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"}, + {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"}, + {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"}, + {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"}, + {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"}, + {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"}, + {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"}, + {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"}, + {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"}, + {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"}, + {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"}, + {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, + {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, + {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, + {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, + {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, + {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, +] + [[package]] name = "multidict" version = "6.0.4" @@ -3797,6 +3904,140 @@ files = [ {file = "pycryptodome-3.18.0.tar.gz", hash = "sha256:c9adee653fc882d98956e33ca2c1fb582e23a8af7ac82fee75bd6113c55a0413"}, ] +[[package]] +name = "pydantic" +version = "2.0.2" +description = "Data validation using Python type hints" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-2.0.2-py3-none-any.whl", hash = "sha256:f5581e0c79b2ec2fa25a9d30d766629811cdda022107fa73d022ab5578873ae3"}, + {file = "pydantic-2.0.2.tar.gz", hash = "sha256:b802f5245b8576315fe619e5989fd083448fa1258638ef9dac301ca60878396d"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.1.2" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.1.2" +description = "" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.1.2-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:b4815720c266e832b20e27a7a5f3772bb09fdedb31a9a34bab7b49d98967ef5a"}, + {file = "pydantic_core-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8884a1dbfc5cb8c54b48446ca916d4577c1f4d901126091e4ab25d00194e065f"}, + {file = "pydantic_core-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74a33aa69d476773230396396afb8e11908f8dafdcfd422e746770599a3f889d"}, + {file = "pydantic_core-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af832edd384755826e494ffdcf1fdda86e4babc42a0b18d342943fb18181040e"}, + {file = "pydantic_core-2.1.2-cp310-cp310-manylinux_2_24_armv7l.whl", hash = "sha256:017700236ea2e7afbef5d3803559c80bd8720306778ebd49268de7ce9972e83e"}, + {file = "pydantic_core-2.1.2-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:c2d00a96fdf26295c6f25eaf9e4a233f353146a73713cd97a5f5dc6090c3aef2"}, + {file = "pydantic_core-2.1.2-cp310-cp310-manylinux_2_24_s390x.whl", hash = "sha256:2575664f0a559a7b951a518f6f34c23cab7190f34f8220b8c8218c4f403147ee"}, + {file = "pydantic_core-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24c3c9180a2d19d640bacc2d00f497a9a1f2abadb2a9ee201b56bb03bc5343bd"}, + {file = "pydantic_core-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:88a56f0f6d020b4d17641f4b4d1f9540a536d4146768d059c430e97bdb485fc1"}, + {file = "pydantic_core-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fa38a76e832743866aed6b715869757074b06357d1a260163ec26d84974245fe"}, + {file = "pydantic_core-2.1.2-cp310-none-win32.whl", hash = "sha256:a772c652603855d7180015849d483a1f539351a263bb9b81bfe85193a33ce124"}, + {file = "pydantic_core-2.1.2-cp310-none-win_amd64.whl", hash = "sha256:b4673d1f29487608d613ebcc5caa99ba15eb58450a7449fb6d800f29d90bebc1"}, + {file = "pydantic_core-2.1.2-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:76c9c55462740d728b344e3a087775846516c3fee31ec56e2075faa7cfcafcbf"}, + {file = "pydantic_core-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb854ec52e6e2e05b83d647695f4d913452fdd45a3dfa8233d7dab5967b3908f"}, + {file = "pydantic_core-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ac140d54da366672f6b91f9a1e8e2d4e7e72720143353501ae886d3fca03272"}, + {file = "pydantic_core-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:818f5cb1b209ab1295087c45717178f4bbbd2bd7eda421f7a119e7b9b736a3cb"}, + {file = "pydantic_core-2.1.2-cp311-cp311-manylinux_2_24_armv7l.whl", hash = "sha256:db4564aea8b3cb6cf1e5f3fd80f1ced73a255d492396d1bd8abd688795b34d63"}, + {file = "pydantic_core-2.1.2-cp311-cp311-manylinux_2_24_ppc64le.whl", hash = "sha256:2ca2d2d5ab65fb40dd05259965006edcc62a9d9b30102737c0a6f45bcbd254e8"}, + {file = "pydantic_core-2.1.2-cp311-cp311-manylinux_2_24_s390x.whl", hash = "sha256:7c7ad8958aadfbcd664078002246796ecd5566b64b22f6af4fd1bbcec6bf8f60"}, + {file = "pydantic_core-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:080a7af828388284a68ad7d3d3eac3bcfff6a580292849aff087e7d556ec42d4"}, + {file = "pydantic_core-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bad7029fb2251c1ac7d3acdd607e540d40d137a7d43a5e5acdcfdbd38db3fc0a"}, + {file = "pydantic_core-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1635a37137fafbc6ee0a8c879857e05b30b1aabaa927e653872b71f1501b1502"}, + {file = "pydantic_core-2.1.2-cp311-none-win32.whl", hash = "sha256:eb4301f009a44bb5db5edfe4e51a8175a4112b566baec07f4af8b1f8cb4649a2"}, + {file = "pydantic_core-2.1.2-cp311-none-win_amd64.whl", hash = "sha256:ebf583f4d9b52abd15cc59e5f6eeca7e3e9741c6ea62d8711c00ac3acb067875"}, + {file = "pydantic_core-2.1.2-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:90b06bb47e60173d24c7cb79670aa8dd6081797290353b9d3c66d3a23e88eb34"}, + {file = "pydantic_core-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e5761ce986ec709897b1b965fad9743f301500434bea3cbab2b6e662571580f"}, + {file = "pydantic_core-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9f8bf1d7008a58fbb6eb334dc6e2f2905400cced8dadb46c4ca28f005a8562"}, + {file = "pydantic_core-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a014ee88980013d192a718cbb88e8cea20acd3afad69bc6d15672d05a49cdb6"}, + {file = "pydantic_core-2.1.2-cp312-cp312-manylinux_2_24_armv7l.whl", hash = "sha256:8125152b03dd91deca5afe5b933a1994b39405adf6be2fe8dce3632319283f85"}, + {file = "pydantic_core-2.1.2-cp312-cp312-manylinux_2_24_ppc64le.whl", hash = "sha256:dc737506b4a0ba2922a2626fc6d620ce50a46aebd0fe2fbcad1b93bbdd8c7e78"}, + {file = "pydantic_core-2.1.2-cp312-cp312-manylinux_2_24_s390x.whl", hash = "sha256:bb471ea8650796060afc99909d9b75da583d317e52f660faf64c45f70b3bf1e2"}, + {file = "pydantic_core-2.1.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1fad38db1744d27061df516e59c5025b09b0a50a337c04e6eebdbddc18951bc"}, + {file = "pydantic_core-2.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:94d368af9e6563de6e7170a74710a2cbace7a1e9c8e507d9e3ac34c7065d7ae3"}, + {file = "pydantic_core-2.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bd95d223de5162811a7b36c73d48eac4fee03b075132f3a1b73c132ce157a60c"}, + {file = "pydantic_core-2.1.2-cp312-none-win32.whl", hash = "sha256:cd62f73830d4715bc643ae39de0bd4fb9c81d6d743530074da91e77a2cccfe67"}, + {file = "pydantic_core-2.1.2-cp312-none-win_amd64.whl", hash = "sha256:51968887d6bd1eaa7fc7759701ea8ccb470c04654beaa8ede6835b0533f206a9"}, + {file = "pydantic_core-2.1.2-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:7ff6bfe63f447a509ed4d368a7f4ba6a7abc03bc4744fc3fb30f2ffab73f3821"}, + {file = "pydantic_core-2.1.2-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:4e67f9b9dfda2e42b39459cbf99d319ccb90da151e35cead3521975b2afbf673"}, + {file = "pydantic_core-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b815a769b019dd96be6571096f246b74f63330547e9b30244c51b4a2eb0277fc"}, + {file = "pydantic_core-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aff436c23c68449601b3fba7075b4f37ef8fbb893c8c1ed3ef898f090332b1e"}, + {file = "pydantic_core-2.1.2-cp37-cp37m-manylinux_2_24_armv7l.whl", hash = "sha256:2ee3ae58f271851362f6c9b33e4c9f9e866557ec7d8c03dc091e9b5aa5566cec"}, + {file = "pydantic_core-2.1.2-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:cf92dccca8f66e987f6c4378700447f82b79e86407912ab1ee06b16b82f05120"}, + {file = "pydantic_core-2.1.2-cp37-cp37m-manylinux_2_24_s390x.whl", hash = "sha256:4663293a36a851a860b1299c50837914269fca127434911297dd39fea9667a01"}, + {file = "pydantic_core-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1c917f7a41d9d09b8b024a5d65cf37e5588ccdb6e610d2df565fb7186b1f3b1c"}, + {file = "pydantic_core-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:06ae67547251135a1b3f8dd465797b13146295a3866bc12ddd73f7512787bb7c"}, + {file = "pydantic_core-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4938b32c09dbcecbeb652327cb4a449b1ef1a1bf6c8fc2c8241aa6b8f6d63b54"}, + {file = "pydantic_core-2.1.2-cp37-none-win32.whl", hash = "sha256:682ff9228c838018c47dfa89b3d84cca45f88cacde28807ab8296ec221862af4"}, + {file = "pydantic_core-2.1.2-cp37-none-win_amd64.whl", hash = "sha256:6e3bcb4a9bc209a61ea2aceb7433ce2ece32c7e670b0c06848bf870c9b3e7d87"}, + {file = "pydantic_core-2.1.2-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:2278ca0b0dfbcfb1e12fa58570916dc260dc72bee5e6e342debf5329d8204688"}, + {file = "pydantic_core-2.1.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87cff210af3258ca0c829e3ebc849d7981bfde23a99d6cb7a3c17a163b3dbad2"}, + {file = "pydantic_core-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7684b5fb906b37e940c5df3f57118f32e033af5e4770e5ae2ae56fbd2fe1a30a"}, + {file = "pydantic_core-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3747a4178139ebf3f19541285b2eb7c886890ca4eb7eec851578c02a13cc1385"}, + {file = "pydantic_core-2.1.2-cp38-cp38-manylinux_2_24_armv7l.whl", hash = "sha256:e17056390068afd4583d88dcf4d4495764e4e2c7d756464468e0d21abcb8931e"}, + {file = "pydantic_core-2.1.2-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:c720e55cef609d50418bdfdfb5c44a76efc020ae7455505788d0113c54c7df55"}, + {file = "pydantic_core-2.1.2-cp38-cp38-manylinux_2_24_s390x.whl", hash = "sha256:b59a64c367f350873c40a126ffe9184d903d2126c701380b4b55753484df5948"}, + {file = "pydantic_core-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68a2a767953c707d9575dcf14d8edee7930527ee0141a8bb612c22d1f1059f9a"}, + {file = "pydantic_core-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae46769d9a7138d58cd190441cac14ce954010a0081f28462ed916c8e55a4f"}, + {file = "pydantic_core-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc909f62325a631e1401dd07dfc386986dbcac15f98c9ff2145d930678a9d25a"}, + {file = "pydantic_core-2.1.2-cp38-none-win32.whl", hash = "sha256:b4038869ba1d8fa33863b4b1286ab07e6075a641ae269b865f94d7e10b3e800e"}, + {file = "pydantic_core-2.1.2-cp38-none-win_amd64.whl", hash = "sha256:5948af62f323252d56acaec8ebfca5f15933f6b72f8dbe3bf21ee97b2d10e3f0"}, + {file = "pydantic_core-2.1.2-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:8e6ce261ccb9a986953c4dce070327e4954f9dd4cd214746dfc70efbc713b6a1"}, + {file = "pydantic_core-2.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d35d634d9d1ed280c87bc2a7a6217b8787eedc86f368fc2fa1c0c8c78f7d3c93"}, + {file = "pydantic_core-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be2e2812a43205728a06c9d0fd090432cd76a9bb5bff2bfcfdf8b0e27d51851"}, + {file = "pydantic_core-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0eb54b11cd4fe0c6404611eef77086ade03eb1457e92910bbb4f3479efa3f79"}, + {file = "pydantic_core-2.1.2-cp39-cp39-manylinux_2_24_armv7l.whl", hash = "sha256:087ddbb754575618a8832ee4ab52fe7eb332f502e2a56088b53dbeb5c4efdf9f"}, + {file = "pydantic_core-2.1.2-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:b74906e01c7fc938ac889588ef438de812989817095c3c4904721f647d64a4d1"}, + {file = "pydantic_core-2.1.2-cp39-cp39-manylinux_2_24_s390x.whl", hash = "sha256:60b7239206a2f61ad89c7518adfacb3ccd6662eaa07c5e437317aea2615a1f18"}, + {file = "pydantic_core-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:be3419204952bbe9b72b90008977379c52f99ae1c6e640488de4be783c345d71"}, + {file = "pydantic_core-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:804cf8f6a859620f8eb754c02f7770f61c3e9c519f8338c331d555b3d6976e3c"}, + {file = "pydantic_core-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cbba32fb14e199d0493c6b9c44870dab0a9c37af9f0f729068459d1849279ffd"}, + {file = "pydantic_core-2.1.2-cp39-none-win32.whl", hash = "sha256:6bf00f56a4468f5b03dadb672a5f1d24aea303d4ccffe8a0f548c9e36017edd3"}, + {file = "pydantic_core-2.1.2-cp39-none-win_amd64.whl", hash = "sha256:ac462a28218ea7d592c7ad51b517558f4ac6565a4e53db7a4811eeaf9c9660b0"}, + {file = "pydantic_core-2.1.2-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:047e782b9918f35ef534ced36f1fd2064f5581229b7a15e4d3177387a6b53134"}, + {file = "pydantic_core-2.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c0213891898fa5b404cf3edf4797e3ac7819a0708ea5473fc6432a2aa27c189"}, + {file = "pydantic_core-2.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0f481aaf0119f77b200e5a5e2799b3e14c015a317eaa948f42263908735cc9f"}, + {file = "pydantic_core-2.1.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15eb4cb543ed36f6a4f16e3bee7aa7ed1c3757be95a3f3bbb2b82b9887131e0f"}, + {file = "pydantic_core-2.1.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ef71e73a81a4cd7e87c93e8ff0170140fd93ba33b0f61e83da3f55f6e0a84fb4"}, + {file = "pydantic_core-2.1.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:840238c845b0f80777151fef0003088ab91c6f7b3467edaff4932b425c4e3c3f"}, + {file = "pydantic_core-2.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7648e48ba263ca0a8a2dc55a60a219c9133fb101ba52c89a14a29fb3d4322ca3"}, + {file = "pydantic_core-2.1.2-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:8eb4e2b71562375609c66a79f89acd4fe95c5cba23473d04952c8b14b6f908f5"}, + {file = "pydantic_core-2.1.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056afea59651c4e47ec6dadbb77ccae4742c059a3d12bc1c0e393d189d2970d"}, + {file = "pydantic_core-2.1.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46cd323371aa7e4053010ccdb94063a4273aa9e5dbe97f8a1147faa769de8d8d"}, + {file = "pydantic_core-2.1.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa39499625239da4ec960cf4fc66b023929b24cc77fb8520289cfdb3c1986428"}, + {file = "pydantic_core-2.1.2-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f5de2d4167fd4bc5ad205fb7297e25867b8e335ca08d64ed7a561d2955a2c32d"}, + {file = "pydantic_core-2.1.2-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:9a5fba9168fc27805553760fa8198db46eef83bf52b4e87ebbe1333b823d0e70"}, + {file = "pydantic_core-2.1.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e68a404fad8493989d6f07b7b9e066f1d2524d7cb64db2d4e9a84c920032c67f"}, + {file = "pydantic_core-2.1.2-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:1a5c4475510d1a9cc1458a26cfc21442223e52ce9adb640775c38739315d03c7"}, + {file = "pydantic_core-2.1.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0681472245ef182554208a25d16884c84f1c5a69f14e6169b88932e5da739a1c"}, + {file = "pydantic_core-2.1.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7fd334b40c5e13a97becfcaba314de0dcc6f7fe21ec8f992139bcc64700e9dc"}, + {file = "pydantic_core-2.1.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7345b1741bf66a9d8ed0ec291c3eabd534444e139e1ea6db5742ac9fd3be2530"}, + {file = "pydantic_core-2.1.2-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0855cf8b760fb40f97f0226cb527c8a94a2ab9d8179628beae20d6939aaeacb0"}, + {file = "pydantic_core-2.1.2-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d281a10837d98db997c0247f45d138522c91ce30cf3ae7a6afdb5e709707d360"}, + {file = "pydantic_core-2.1.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:82e09f27edab289187dd924d4d93f2a35f21aa969699b2504aa643da7fbfeff9"}, + {file = "pydantic_core-2.1.2-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:aa54902fa51f7d921ba80923cf1c7ff3dce796a7903300bd8824deb90e357744"}, + {file = "pydantic_core-2.1.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b9a5fc4058d64c9c826684dcdb43891c1b474a4a88dcf8dfc3e1fb5889496f8"}, + {file = "pydantic_core-2.1.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:817681d111cb65f07d46496eafec815f48e1aff37713b73135a0a9eb4d3610ab"}, + {file = "pydantic_core-2.1.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b5d37aedea5963f2097bddbcdb255483191646a52d40d8bb66d61c190fcac91"}, + {file = "pydantic_core-2.1.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f2de65752fff248319bcd3b29da24e205fa505607539fcd4acc4037355175b63"}, + {file = "pydantic_core-2.1.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:a8b9c2cc4c5f8169b943d24be4bd1548fe81c016d704126e3a3124a2fc164885"}, + {file = "pydantic_core-2.1.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f7bcdf70c8b6e70be11c78d3c00b80a24cccfb408128f23e91ec3019bed1ecc1"}, + {file = "pydantic_core-2.1.2.tar.gz", hash = "sha256:d2c790f0d928b672484eac4f5696dd0b78f3d6d148a641ea196eb49c0875e30a"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pydotplus" version = "2.0.2" @@ -5650,4 +5891,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "538c0233deaa23ceee9a4b0dd547ffd759317b54cd5908129a9786c724a696bd" +content-hash = "d06b2a1ccbc8c661df8bc425ccab27516a6dfea1b550976d1338292b47ce8f83" diff --git a/pyproject.toml b/pyproject.toml index 9f74f7d..8713e49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,6 +91,8 @@ django-tables2 = "^2.5.3" django-filter = "^23.2" tablib = "^3.4.0" django-location-field = "^2.7.0" +pydantic = "^2.0.2" +channels-redis = "^4.1.0" [build-system]