1 Commits

Author SHA1 Message Date
196eb73157 Sort tracks based on title, then on artist 2023-10-06 20:45:14 +02:00
52 changed files with 385 additions and 1278 deletions

5
.gitignore vendored
View File

@ -140,7 +140,4 @@ dmypy.json
cython_debug/
# Jetbrains
.idea/
# macOS
.DS_Store
.idea/

View File

@ -12,7 +12,7 @@ black:
- python3 -m pip install --upgrade pip
- curl -sSL https://install.python-poetry.org | python3 -
- export PATH="/root/.local/bin:$PATH"
- poetry install --with dev --no-root
- poetry install --with dev
script:
- poetry run black --quiet --check marietje

View File

@ -1,16 +0,0 @@
from django.contrib import admin
from announcements.models import Announcement
@admin.register(Announcement)
class AnnouncementAdmin(admin.ModelAdmin):
"""Manage the admin pages for the announcements."""
list_display = ("title", "since", "until", "visible")
def visible(self, obj):
"""Is the object visible."""
return obj.is_visible
visible.boolean = True

View File

@ -1,8 +0,0 @@
from django.apps import AppConfig
class AnnouncementsConfig(AppConfig):
"""Announcements AppConfig."""
default_auto_field = "django.db.models.BigAutoField"
name = "announcements"

View File

@ -1,43 +0,0 @@
from announcements.services import (
validate_closed_announcements,
sanitize_closed_announcements,
encode_closed_announcements,
)
from django.apps import apps
class ClosedAnnouncementsMiddleware:
"""Closed Announcements Middleware."""
def __init__(self, get_response):
"""Initialize."""
self.get_response = get_response
def __call__(self, request):
"""Update the closed announcements' cookie."""
response = self.get_response(request)
closed_announcements = validate_closed_announcements(
sanitize_closed_announcements(request.COOKIES.get("closed-announcements", None))
)
response.set_cookie("closed-announcements", encode_closed_announcements(closed_announcements))
return response
class AppAnnouncementMiddleware:
"""Announcement Middleware."""
def __init__(self, get_response):
"""Initialize Announcement Middleware."""
self.get_response = get_response
def __call__(self, request):
"""Call app announcement function if they exist for gathering all announcements."""
announcements = []
for app in apps.get_app_configs():
if hasattr(app, "announcements") and hasattr(app.announcements, "__call__"):
announcements += app.announcements(request)
setattr(request, "_app_announcements", announcements)
return self.get_response(request)

View File

@ -1,30 +0,0 @@
# Generated by Django 3.2.16 on 2022-12-09 19:50
from django.db import migrations, models
import django.utils.timezone
import tinymce.models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Announcement',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(help_text='This is not shown on the announcement but can be used as an identifier in the admin area.', max_length=100)),
('content', tinymce.models.HTMLField(max_length=500)),
('since', models.DateTimeField(default=django.utils.timezone.now)),
('until', models.DateTimeField(blank=True, null=True)),
('icon', models.CharField(default='bullhorn', help_text='Font Awesome 6 abbreviation for icon to use.', max_length=150, verbose_name='Font Awesome 6 icon')),
],
options={
'ordering': ('-since',),
},
),
]

View File

@ -1,47 +0,0 @@
from django.db import models
from django.db.models import Q
from django.utils import timezone
from tinymce.models import HTMLField
class AnnouncementManager(models.Manager):
"""Announcement Manager."""
def visible(self):
"""Get only visible announcements."""
return self.get_queryset().filter((Q(until__gt=timezone.now()) | Q(until=None)) & Q(since__lte=timezone.now()))
class Announcement(models.Model):
"""Announcement model."""
title = models.CharField(
max_length=100,
help_text="This is not shown on the announcement but can be used as an identifier in the admin area.",
)
content = HTMLField(blank=False, max_length=500)
since = models.DateTimeField(default=timezone.now)
until = models.DateTimeField(blank=True, null=True)
icon = models.CharField(
verbose_name="Font Awesome 6 icon",
help_text="Font Awesome 6 abbreviation for icon to use.",
max_length=150,
default="bullhorn",
)
objects = AnnouncementManager()
class Meta:
"""Meta class."""
ordering = ("-since",)
def __str__(self):
"""Cast this object to string."""
return self.title
@property
def is_visible(self):
"""Is this announcement currently visible."""
return (self.until is None or self.until > timezone.now()) and self.since <= timezone.now()

View File

@ -1,33 +0,0 @@
import json
import urllib.parse
from announcements.models import Announcement
def sanitize_closed_announcements(closed_announcements) -> list:
"""Convert a cookie (closed_announcements) to a list of id's of closed announcements."""
if closed_announcements is None or not isinstance(closed_announcements, str):
return []
try:
closed_announcements_list = json.loads(urllib.parse.unquote(closed_announcements))
except json.JSONDecodeError:
return []
if not isinstance(closed_announcements_list, list):
return []
closed_announcements_list_ints = []
for closed_announcement in closed_announcements_list:
if isinstance(closed_announcement, int):
closed_announcements_list_ints.append(closed_announcement)
return closed_announcements_list_ints
def validate_closed_announcements(closed_announcements) -> list:
"""Verify the integers in the list such that the ID's that in the database exist only remain."""
return list(Announcement.objects.filter(id__in=closed_announcements).values_list("id", flat=True))
def encode_closed_announcements(closed_announcements: list) -> str:
"""Encode the announcement list in URL encoding."""
return urllib.parse.quote(json.dumps(closed_announcements))

View File

@ -1,34 +0,0 @@
.announcement {
display: flex;
justify-content: center;
align-items: center;
background: var(--primary-shade);
color: var(--primary-contrast);
text-align: center;
padding: 0.5rem 1rem;
}
.announcement .btn-close {
color: var(--primary-contrast);
}
.announcement p {
display: inline;
margin: 0;
color: var(--primary-contrast);
font-size: 0.9rem;
font-weight: bold;
}
.announcement a {
color: var(--primary-contrast);
text-decoration: underline;
}
.announcement a:hover {
color: var(--primary-contrast-hover);
}
.announcement button {
background-size: .6rem;
}

View File

@ -1,53 +0,0 @@
const ANNOUNCEMENT_COOKIE = "closed-announcements";
function safeGetCookie() {
try {
return getCookie(ANNOUNCEMENT_COOKIE);
} catch {
return null;
}
}
function sanitizeAnnouncementsArray(closedAnnouncements) {
if (closedAnnouncements === null || typeof closedAnnouncements !== 'string') {
return [];
}
else {
try {
closedAnnouncements = JSON.parse(closedAnnouncements);
} catch {
return [];
}
if (! Array.isArray(closedAnnouncements)) {
closedAnnouncements = [];
}
for (let i = 0; i < closedAnnouncements.length; i++) {
if (!Number.isInteger(closedAnnouncements[i])) {
closedAnnouncements.splice(i, 1);
}
}
return closedAnnouncements;
}
}
function close_announcement(announcementId) {
let announcementElement = document.getElementById("announcement-" + announcementId);
if (announcementElement !== null) {
announcementElement.remove();
}
let closedAnnouncements = safeGetCookie();
if (closedAnnouncements === null) {
closedAnnouncements = [];
} else {
closedAnnouncements = sanitizeAnnouncementsArray(closedAnnouncements);
}
if (!closedAnnouncements.includes(announcementId)) {
closedAnnouncements.push(announcementId);
}
setListCookie(ANNOUNCEMENT_COOKIE, closedAnnouncements, 31);
}

View File

@ -1,15 +0,0 @@
{% load bleach_tags %}
{% for announcement in announcements %}
<div class="announcement" id="announcement-{{ announcement.id }}">
<i class="fas fa-{{ announcement.icon }} me-3"></i> {{ announcement.content|bleach }}
<button type="button" class="btn-close ms-3" aria-label="Close"
data-announcement-id="{{ announcement.pk }}" onclick="close_announcement({{ announcement.id }})"></button>
</div>
{% endfor %}
{% for announcement in app_announcements %}
<div class="announcement">
{{ announcement|bleach }}
</div>
{% endfor %}

View File

@ -1,20 +0,0 @@
from django import template
from django.db.models import Q
from announcements.models import Announcement
from announcements.services import sanitize_closed_announcements
register = template.Library()
@register.inclusion_tag("announcements/announcements.html", takes_context=True)
def render_announcements(context):
"""Render all active announcements."""
request = context.get("request")
closed_announcements = sanitize_closed_announcements(request.COOKIES.get("closed-announcements", None))
return {
"announcements": Announcement.objects.visible().filter(~Q(id__in=closed_announcements)),
"app_announcements": getattr(request, "_app_announcements", []),
"closed_announcements": closed_announcements,
}

View File

@ -1,41 +0,0 @@
from bleach.css_sanitizer import CSSSanitizer
from django import template
from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe
from bleach import clean
register = template.Library()
@register.filter(is_safe=True)
@stringfilter
def bleach(value):
"""Bleach dangerous html from the input."""
css_sanitizer = CSSSanitizer(allowed_css_properties=["text-decoration"])
return mark_safe(
clean(
value,
tags=[
"h2",
"h3",
"p",
"a",
"div",
"strong",
"em",
"i",
"b",
"ul",
"li",
"br",
"ol",
"span",
],
attributes={
"*": ["class", "style"],
"a": ["href", "rel", "target", "title"],
},
css_sanitizer=css_sanitizer,
strip=True,
)
)

View File

@ -1,43 +0,0 @@
from django.test import TestCase
from announcements import models
from announcements.services import sanitize_closed_announcements, validate_closed_announcements
class OrderServicesTests(TestCase):
def test_sanitize_closed_announcements_none(self):
self.assertEquals([], sanitize_closed_announcements(None))
def test_sanitize_closed_announcements_non_string(self):
self.assertEquals([], sanitize_closed_announcements(5))
def test_sanitize_closed_announcements_non_json(self):
self.assertEquals([], sanitize_closed_announcements("this,is,not,json"))
def test_sanitize_closed_announcements_non_list(self):
self.assertEquals([], sanitize_closed_announcements("{}"))
def test_sanitize_closed_announcements_list_of_ints(self):
self.assertEquals([1, 8, 4], sanitize_closed_announcements("[1, 8, 4]"))
def test_sanitize_closed_announcements_list_of_different_types(self):
self.assertEquals(
[1, 8, 4], sanitize_closed_announcements('[1, "bla", 8, 4, 123.4, {"a": "b"}, ["This is also a list"]]')
)
def test_validate_closed_announcements_all_exist(self):
announcement_1 = models.Announcement.objects.create(title="Announcement 1", content="blablabla")
announcement_2 = models.Announcement.objects.create(title="Announcement 2", content="blablabla")
announcement_3 = models.Announcement.objects.create(title="Announcement 3", content="blablabla")
self.assertEqual(
{announcement_1.id, announcement_2.id, announcement_3.id}, set(validate_closed_announcements([1, 2, 3]))
)
def test_validate_closed_announcements_some_do_not_exist(self):
announcement_1 = models.Announcement.objects.create(title="Announcement 1", content="blablabla")
announcement_2 = models.Announcement.objects.create(title="Announcement 2", content="blablabla")
announcement_3 = models.Announcement.objects.create(title="Announcement 3", content="blablabla")
self.assertEqual(
{announcement_1.id, announcement_2.id, announcement_3.id},
set(validate_closed_announcements([1, 2, 3, 4, 5, 6])),
)

View File

@ -3,13 +3,7 @@ import os
import sys
if __name__ == "__main__":
try:
import marietje.settings.production # noqa
except ModuleNotFoundError:
# Use the development settings if the production settings are not available (so we're on a dev machine)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "marietje.settings.development")
else:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "marietje.settings.production")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "marietje.settings.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
@ -17,7 +11,7 @@ if __name__ == "__main__":
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django # noqa
import django
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "

View File

@ -7,13 +7,13 @@ from .models import User
@admin.register(User)
class UserAdmin(BaseUserAdmin):
fieldsets = (
(None, {"fields": ("username", "password")}),
(None, {"fields": ("username", "password", "queue")}),
(_("Personal info"), {"fields": ("name", "email")}),
(_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}),
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
(_("Activation"), {"fields": ("activation_token", "reset_token")}),
)
list_display = ("username", "email", "name", "date_joined", "last_login", "is_staff")
list_display = ("username", "email", "name", "date_joined", "last_login", "queue", "is_staff")
search_fields = ("username", "name", "email")
def delete_model(self, request, user):

View File

@ -28,6 +28,7 @@ class Command(BaseCommand):
user.name = import_user["n"].strip()
user.email = user.username + "@science.ru.nl"
user.password = "md5$$" + import_user["p"]
user.queue = get_first_queue()
user.save()
if options["tsv_file"]:
@ -44,6 +45,7 @@ class Command(BaseCommand):
user.name = import_user[2].decode("utf-8", errors="ignore").strip()
user.email = user.username + "@science.ru.nl"
user.password = import_user[3].decode("utf-8", errors="strict")
user.queue = get_first_queue()
user.study = import_user[5].decode("utf-8", errors="ignore").strip()
user.save()

View File

@ -1,28 +0,0 @@
# Generated by Django 4.2.6 on 2023-11-24 20:17
from django.db import migrations
def create_new_queue_mappings(apps, schema_editor):
"""Before removing the old reference to Queue from User, we should move this to the newly created model."""
User = apps.get_model("marietje", "User")
UserQueue = apps.get_model("queues", "UserQueue")
for user in User.objects.all():
if user.queue is not None:
UserQueue.objects.create(user=user, queue=user.queue)
else:
UserQueue.objects.create(user=user)
class Migration(migrations.Migration):
dependencies = [
("marietje", "0008_alter_user_id"),
("queues", "0012_userqueue_queuelogentry"),
]
operations = [
migrations.RunPython(
create_new_queue_mappings,
migrations.RunPython.noop
),
]

View File

@ -1,16 +0,0 @@
# Generated by Django 4.2.6 on 2023-11-24 20:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("marietje", "0009_auto_20231124_2117"),
]
operations = [
migrations.RemoveField(
model_name="user",
name="queue",
),
]

View File

@ -7,6 +7,9 @@ from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from marietje.utils import get_first_queue
from queues.models import Queue
class UserManager(BaseUserManager):
use_in_migrations = True
@ -16,8 +19,9 @@ class UserManager(BaseUserManager):
raise ValueError("The given username must be set")
email = self.normalize_email(email)
username = self.model.normalize_username(username)
queue = get_first_queue()
user = self.model(username=username, email=email, **extra_fields)
user = self.model(username=username, email=email, queue=queue, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
@ -76,6 +80,8 @@ class User(AbstractBaseUser, PermissionsMixin):
objects = UserManager()
queue = models.ForeignKey(Queue, on_delete=models.SET_NULL, blank=True, null=True)
activation_token = models.TextField(_("activation token"), blank=True, null=True)
reset_token = models.TextField(_("reset token"), blank=True, null=True)

View File

@ -1,20 +0,0 @@
import os
from .base import *
SECRET_KEY = 'sae2hahHao1soo0Ocoz5Ieh1Ushae6feJe4mooliooj0Ula8'
DEBUG = True
ALLOWED_HOSTS = ['*']
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
BERTHA_HOST = ('localhost', 1234)
BASE_URL = 'http://localhost:8000'

View File

@ -1,23 +0,0 @@
from .base import *
SECRET_KEY = '******'
DEBUG = False
ALLOWED_HOSTS = ['marietje-zuid.nl']
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'marietje',
'USER': 'marietje',
'PASSWORD': '******',
'HOST': 'localhost',
'PORT': '3306',
'OPTIONS': {'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"},
}
}
BASE_URL = 'https://marietje-zuid.science.ru.nl'
BERTHA_HOST = ('bach.science.ru.nl', 1234)

View File

@ -1,10 +1,16 @@
import os
from pathlib import Path
"""
Django settings for marietje project.
"""
BASE_DIR = Path(__file__).resolve().parent.parent.parent
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
SECRET_KEY = 'sae2hahHao1soo0Ocoz5Ieh1Ushae6feJe4mooliooj0Ula8'
DEBUG = False
ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
'marietje',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@ -14,8 +20,7 @@ INSTALLED_APPS = [
'django_bootstrap5',
'fontawesomefree',
'rest_framework',
'tinymce',
'announcements',
'marietje',
'queues',
'songs',
'stats',
@ -59,6 +64,18 @@ TEMPLATES = [
WSGI_APPLICATION = 'marietje.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'marietje',
'USER': 'marietje',
'PASSWORD': 'v8TzZwdAdSi7Tk5I',
'HOST': 'localhost',
'PORT': '3306',
'OPTIONS': {'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"},
}
}
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
@ -130,6 +147,12 @@ OAUTH2_PROVIDER = {
},
}
# zc files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
BASE_URL = 'https://marietje-zuid.science.ru.nl'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/'
LOGIN_URL = '/login/'
@ -141,8 +164,10 @@ LOGOUT_REDIRECT_URL = '/'
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
MAIL_FROM = 'marietje@marietje.science.ru.nl'
# App specific settings
BERTHA_HOST = ('bach.science.ru.nl', 1234)
MAIL_FROM = 'marietje@marietje.science.ru.nl'
MAX_MINUTES_IN_A_ROW = 45
# Time range (dependent on timezone specified) when MAX_MINUTES_IN_A_ROW is in effect.

View File

@ -100,17 +100,10 @@ footer {
transition: 1s transform ease-in-out;
}
.currentsong {
.currentsong{
border-bottom: 1px solid #DDDDDD;
}
.navbar-text {
color: var(--text-color);
}
.danger {
color: red !important;
}
/* Bootstrap 3 doesn't support equal height columns, hack via <https://medium.com/wdstack/bootstrap-equal-height-columns-d07bc934eb27#892f> */
.row.display-flex {
display: flex;

View File

@ -1,9 +1,4 @@
:root {
--pimary: #0d6efd;
--primary-shade: #0d54bd;
--primary-contrast: #ffffff;
--primary-contrast-hover: rgba(255, 255, 255, 0.7);
--background-color: #ffffff;
--background-shade: #dddddd;
--background-shade-light: #f7f7f7;

View File

@ -36,37 +36,4 @@ Number.prototype.timestampToHHMMSS = function () {
seconds = '0' + seconds;
}
return hours + ':' + minutes + ':' + seconds;
};
function setCookie(name,value,days) {
let expires = "";
value = encodeURI(value);
if (days) {
let date = new Date();
date.setTime(date.getTime() + (days*24*60*60*1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function getCookie(name) {
let nameEQ = name + "=";
let ca = document.cookie.split(';');
for(let i=0;i < ca.length;i++) {
let c = ca[i];
while (c.charAt(0)===' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length,c.length));
}
return null;
}
function setListCookie(name, list, days) {
try {
let string = JSON.stringify(list);
setCookie(name, string, days);
return true;
}
catch(error) {
return false;
}
}
};

View File

@ -1,4 +1,4 @@
{% load static django_bootstrap5 announcements %}
{% load static django_bootstrap5 %}
<!DOCTYPE html>
<html lang="en">
<head>
@ -22,19 +22,8 @@
<!-- Base JavaScript -->
<script type="text/javascript" src="{% static "marietje/js/base.js" %}"></script>
<!-- Announcements static files -->
<link rel="stylesheet" href="{% static 'announcements/css/announcements.css' %}" type="text/css">
<script src="{% static 'announcements/js/announcements.js' %}"></script>
<script>
const CSRF_TOKEN = "{{ csrf_token }}";
</script>
</head>
<body>
<section id="announcements-alerts">
{% render_announcements %}
</section>
<nav class="navbar navbar-expand-lg sticky-top navbar-dark bg-primary">
<div class="container">
<button class="navbar-toggler ms-auto" type="button" data-bs-toggle="offcanvas"
@ -120,6 +109,9 @@
</footer>
{% endif %}
{% bootstrap_javascript %}
<script>
const CSRF_TOKEN = "{{ csrf_token }}";
</script>
{% block js %}{% endblock %}
</body>
</html>

View File

@ -59,13 +59,7 @@
return response.json();
})
.then(json => {
let requestor;
if(json.current_song.user === null) {
requestor = "Marietje";
} else {
requestor = json.current_song.user.name;
}
updateScreen(requestor, json.current_song.song.artist, json.current_song.song.title);
updateScreen(json.current_song.user.name, json.current_song.song.artist, json.current_song.song.title);
setTimeout(fetchCurrentQueue, 1000);
}).catch(err => {
if(err.name == "NotLoggedIn") {

View File

@ -31,7 +31,7 @@ urlpatterns = [
path("register/", RegisterView.as_view(), name="register"),
path("activate/<int:user_id>/<str:token>/", ActivateView.as_view(), name="activate"),
path("forgotpassword/", ForgotPasswordView.as_view(), name="forgotpassword"),
path("resetpassword/<int:user_id>/<str:token>/", ResetPasswordView.as_view(), name="resetpassword"),
path("resetpassword/<int:user_id>/<str:token>/", ResetPasswordView.as_view, name="resetpassword"),
path("admin/", admin.site.urls),
path("privacy/", PrivacyView.as_view(), name="privacy"),
path("metrics/", metrics, name="metrics"),

View File

@ -0,0 +1,82 @@
import binascii
import socket
import struct
from django.conf import settings
from django.http import StreamingHttpResponse
from queues.models import Queue, Playlist
def song_to_dict(song, include_hash=False, include_user=False, include_replaygain=False, **options):
data = {
"id": song.id,
"artist": song.artist,
"title": song.title,
"duration": song.duration,
}
if include_hash:
data["hash"] = song.hash
if include_user is not None and song.user is not None and song.user.name:
data["uploader_name"] = song.user.name
if include_replaygain:
data["rg_gain"] = song.rg_gain
data["rg_peak"] = song.rg_peak
return data
def playlist_song_to_dict(playlist_song, **options):
user = options.get("user")
return {
"id": playlist_song.id,
"requested_by": "Marietje" if playlist_song.user is None else playlist_song.user.name,
"song": song_to_dict(playlist_song.song, **options),
"can_move_down": playlist_song.user is not None and playlist_song.user == user,
}
# Send a file to bertha file storage.
def send_to_bertha(file):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(settings.BERTHA_HOST)
sock.sendall(struct.pack("<BQ", 4, file.size))
for chunk in file.chunks():
sock.sendall(chunk)
sock.shutdown(socket.SHUT_WR)
song_hash = binascii.hexlify(sock.recv(64))
sock.close()
return song_hash
def get_first_queue():
queue = Queue.objects.first()
if queue is None:
playlist = Playlist()
playlist.save()
random_playlist = Playlist()
random_playlist.save()
queue = Queue(name="Queue", playlist=playlist, random_playlist=random_playlist)
queue.save()
return queue
def bertha_stream(song_hash):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(settings.BERTHA_HOST)
sock.sendall(struct.pack("<B", 2) + binascii.unhexlify(song_hash))
data = sock.recv(4096)
while data:
yield data
data = sock.recv(4096)
sock.close()
def get_from_bertha(song_hash):
response = StreamingHttpResponse(bertha_stream(song_hash))
response["Content-Disposition"] = 'attachment; filename="{}"'.format(song_hash)
return response

View File

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "marietje.settings.production")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "marietje.settings.settings")
application = get_wsgi_application()

View File

@ -1,29 +0,0 @@
def song_to_dict(song, include_hash=False, include_user=False, include_replaygain=False, **options):
data = {
"id": song.id,
"artist": song.artist,
"title": song.title,
"duration": song.duration,
}
if include_hash:
data["hash"] = song.hash
if include_user is not None and song.user is not None and song.user.name:
data["uploader_name"] = song.user.name
if include_replaygain:
data["rg_gain"] = song.rg_gain
data["rg_peak"] = song.rg_peak
return data
def playlist_song_to_dict(playlist_song, **options):
user = options.get("user")
return {
"id": playlist_song.id,
"requested_by": "Marietje" if playlist_song.user is None else playlist_song.user.name,
"song": song_to_dict(playlist_song.song, **options),
"can_move_down": playlist_song.user is not None and playlist_song.user == user,
}

View File

@ -4,11 +4,11 @@ from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from marietje.utils import playlist_song_to_dict
from queues.models import Queue
from songs.models import Song
from .decorators import token_required
from .services import playlist_song_to_dict
@csrf_exempt

View File

@ -1,18 +1,9 @@
from typing import Optional
from django.contrib import admin
from django.contrib.auth import get_user_model
from .models import Queue, Playlist, PlaylistSong, QueueCommand
from .models import Queue, Playlist, PlaylistSong, QueueCommand, QueueLogEntry, UserQueue
from marietje.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext_lazy as _
from .services import get_queue_for_user
admin.site.register(Playlist)
User = get_user_model()
@admin.register(Queue)
class OrderAdmin(admin.ModelAdmin):
@ -30,61 +21,3 @@ class PlaylistSongAdmin(admin.ModelAdmin):
admin.site.register(QueueCommand)
@admin.register(QueueLogEntry)
class QueueLogEntryAdmin(admin.ModelAdmin):
"""Admin for log entries."""
list_display = [
"timestamp",
"queue",
"action",
"user",
"description",
]
list_filter = [
"queue",
"action",
("timestamp", admin.DateFieldListFilter),
]
def has_delete_permission(self, request, obj=None):
"""Disable delete permission."""
return False
def has_add_permission(self, request):
"""Disable add permission."""
return False
def has_change_permission(self, request, obj=None):
"""Disable change permission."""
return False
class UserQueueInline(admin.StackedInline):
model = UserQueue
fields = ("queue",)
class UserAdmin(BaseUserAdmin):
fieldsets = (
(None, {"fields": ("username", "password")}),
(_("Personal info"), {"fields": ("name", "email")}),
(_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}),
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
(_("Activation"), {"fields": ("activation_token", "reset_token")}),
)
list_display = ("username", "email", "name", "date_joined", "last_login", "queue__queue", "is_staff")
inlines = (UserQueueInline,)
def queue__queue(self, obj: User) -> Optional[Queue]:
"""Retrieve the Queue for a User."""
return get_queue_for_user(obj)
queue__queue.short_description = "queue"
admin.site.unregister(User)
admin.site.register(User, UserAdmin)

View File

@ -1,4 +1,3 @@
from django.db.models import Q
from rest_framework.generics import ListAPIView, RetrieveAPIView, get_object_or_404, CreateAPIView, DestroyAPIView
from rest_framework.views import APIView
from rest_framework.response import Response
@ -9,7 +8,7 @@ from django.http import Http404
from queues.api.v1.serializers import PlaylistSerializer, QueueSerializer, PlaylistSongSerializer
from queues.exceptions import RequestException
from queues.models import Playlist, PlaylistSong, QueueCommand, Queue
from queues.models import Playlist, PlaylistSong, QueueCommand
from queues.services import get_user_or_default_queue
from songs.counters import request_counter
from songs.models import Song
@ -81,7 +80,7 @@ class QueueSkipAPIView(APIView):
if queue is None:
return Response(status=404)
playlist_song = queue.current_song()
playlist_song = request.user.queue.current_song()
if (
request.user is not None
and playlist_song.user != request.user
@ -91,7 +90,6 @@ class QueueSkipAPIView(APIView):
playlist_song.state = 2
playlist_song.save()
queue.log_action(request.user, "next", "Skipped to next song.")
return Response(status=200, data=QueueSerializer(queue).data)
@ -113,18 +111,7 @@ class PlaylistSongMoveDownAPIView(APIView):
and not request.user.has_perm("queues.can_move")
):
return Response(status=403)
playlist_song.move_down()
for queue in Queue.objects.filter(
Q(playlist=playlist_song.playlist) | Q(random_playlist=playlist_song.playlist)
):
queue.log_action(
request.user,
"down",
'Moved song "{}" of playlist "{}" down.'.format(playlist_song.song, playlist_song.playlist),
)
return Response(status=200, data=self.serializer_class(playlist_song).data)
@ -144,18 +131,7 @@ class PlaylistSongCancelAPIView(DestroyAPIView):
and not request.user.has_perm("queues.can_cancel")
):
return Response(status=403)
playlist_song.delete()
for queue in Queue.objects.filter(
Q(playlist=playlist_song.playlist) | Q(random_playlist=playlist_song.playlist)
):
queue.log_action(
request.user,
"cancel",
'Cancelled song "{}" of playlist "{}".'.format(playlist_song.song, playlist_song.playlist),
)
return Response(status=200, data=self.serializer_class(playlist_song).data)
@ -189,8 +165,6 @@ class QueueRequestAPIView(CreateAPIView):
except RequestException as e:
return Response(data={"success": False, "errorMessage": str(e)})
queue.log_action(request.user, "request_song", "Requested song {}.".format(song))
request_counter.labels(queue=queue.name).inc()
return Response(status=200, data=self.serializer_class(playlist_song).data)
@ -222,11 +196,7 @@ class QueueVolumeDownAPIView(APIView):
return Response(status=404)
if request.user is not None and not request.user.has_perm("queues.can_control_volume"):
return Response(status=403)
QueueCommand.objects.create(queue=queue, command="volume_down")
queue.log_action(request.user, "volume_down", "Reduced the volume of {}.".format(queue))
return Response(status=200, data=self.serializer_class(queue).data)
@ -257,11 +227,7 @@ class QueueVolumeUpAPIView(APIView):
return Response(status=404)
if request.user is not None and not request.user.has_perm("queues.can_control_volume"):
return Response(status=403)
QueueCommand.objects.create(queue=queue, command="volume_up")
queue.log_action(request.user, "volume_up", "Increased the volume of {}.".format(queue))
return Response(status=200, data=self.serializer_class(queue).data)
@ -292,9 +258,5 @@ class QueueMuteAPIView(APIView):
return Response(status=404)
if request.user is not None and not request.user.has_perm("queues.can_control_volume"):
return Response(status=403)
QueueCommand.objects.create(queue=queue, command="mute")
queue.log_action(request.user, "mute", "Muted the volume of {}.".format(queue))
return Response(status=200, data=self.serializer_class(queue).data)

View File

@ -1,64 +0,0 @@
# Generated by Django 4.2.6 on 2023-11-24 20:17
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("queues", "0011_alter_playlistsong_playlist"),
]
operations = [
migrations.CreateModel(
name="UserQueue",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
(
"queue",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="users",
to="queues.queue",
),
),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="queue_new",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="QueueLogEntry",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("action", models.CharField(max_length=255)),
("timestamp", models.DateTimeField(auto_now_add=True)),
("description", models.CharField(max_length=255)),
(
"queue",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="logs", to="queues.queue"
),
),
(
"user",
models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
"verbose_name": "player log entry",
"verbose_name_plural": "player log entries",
},
),
]

View File

@ -1,22 +0,0 @@
# Generated by Django 4.2.6 on 2023-11-24 20:19
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("queues", "0012_userqueue_queuelogentry"),
]
operations = [
migrations.AlterField(
model_name="userqueue",
name="user",
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE, related_name="queue", to=settings.AUTH_USER_MODEL
),
),
]

View File

@ -1,4 +1,3 @@
from django.contrib.auth import get_user_model
from django.db import models
from django.db.models import Q
from django.conf import settings
@ -7,8 +6,6 @@ from django.utils import timezone
from queues.exceptions import RequestException
from songs.models import Song
User = get_user_model()
class Playlist(models.Model):
def __str__(self):
@ -146,41 +143,10 @@ class Queue(models.Model):
playlist_song.save()
song_count += 1
def log_action(self, user: User, action: str, description: str) -> "QueueLogEntry":
"""
Log a queue action.
:param user: The user performing the action.
:param action: An identifier of the action performed.
:param description: An optional description for the action.
:return: The created QueueLogEntry object.
"""
return QueueLogEntry.objects.create(
queue=self,
user=user,
action=action,
description=description,
)
def __str__(self):
return str(self.name)
class UserQueue(models.Model):
"""
UserQueue model.
This model connects a user to its queue.
"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="queue")
queue = models.ForeignKey(Queue, on_delete=models.SET_NULL, null=True, blank=True, related_name="users")
def __str__(self):
"""Convert this object to string."""
return "Queue for user {}".format(self.user)
class QueueCommand(models.Model):
queue = models.ForeignKey(
Queue,
@ -191,20 +157,3 @@ class QueueCommand(models.Model):
def __str__(self):
return str(self.command)
class QueueLogEntry(models.Model):
"""Model for logging queue events."""
queue = models.ForeignKey(Queue, on_delete=models.CASCADE, related_name="logs")
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
action = models.CharField(max_length=255)
timestamp = models.DateTimeField(auto_now_add=True)
description = models.CharField(max_length=255)
def __str__(self):
return f"{self.queue} {self.action} by {self.user} at {self.timestamp}"
class Meta:
verbose_name = "player log entry"
verbose_name_plural = "player log entries"

View File

@ -1,44 +1,15 @@
from typing import Optional
from django.contrib.auth import get_user_model
from queues.models import Queue, Playlist
from queues.models import Queue
from django.conf import settings
User = get_user_model()
def get_user_or_default_queue(request) -> Queue:
"""Get the user or default queue from a request."""
def get_user_or_default_queue(request):
"""Get the user or default queue."""
if request.user is None:
return get_default_queue()
else:
return get_queue_for_user(request.user)
return request.user.queue
def get_queue_for_user(user: User) -> Optional[Queue]:
"""Get the queue for a User."""
if user.queue is None:
return None
else:
return user.queue.queue
def get_default_queue() -> Queue:
def get_default_queue():
"""Get the default queue."""
try:
return Queue.objects.get(pk=settings.DEFAULT_QUEUE)
except Queue.DoesNotExist:
return get_first_queue()
def get_first_queue() -> Queue:
"""Get the first Queue object or create one."""
queue = Queue.objects.first()
if queue is not None:
return queue
playlist = Playlist.objects.create()
random_playlist = Playlist.objects.create()
return Queue.objects.create(name="Queue", playlist=playlist, random_playlist=random_playlist)
return Queue.objects.get(pk=settings.DEFAULT_QUEUE)

View File

@ -1,18 +0,0 @@
from django.contrib.auth import get_user_model
from django.db.models.signals import post_save
from django.dispatch import receiver
from queues.models import UserQueue
from queues.services import get_default_queue
User = get_user_model()
@receiver(post_save, sender=User)
def create_default_queue(sender, instance, created, **kwargs):
"""Create a UserQueue object when a User gets created."""
if created:
user_queue, user_queue_created = UserQueue.objects.get_or_create(user=instance)
if user_queue_created:
user_queue.queue = get_default_queue()
user_queue.save()

View File

@ -38,26 +38,15 @@
{% endif %}
</li>
</ul>
<ul v-if="'start_personal_queue' in infobar && infobar.start_personal_queue !== null" id="personal-queue-container" class="navbar-nav navbar-right hidden-xs">
<li v-if="infobar.start_personal_queue != 0" class="nav-item me-3">
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
First song starts in <% infobar.start_personal_queue.secondsToMMSS() %>
</p>
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
First song starts at <% (infobar.now_in_seconds + infobar.start_personal_queue).timestampToHHMMSS() %>
</p>
<ul class="navbar-nav navbar-right hidden-xs">
<li class="nav-item me-3">
<p class="navbar-text mb-0 start-queue hidden-sm hidden-xs"></p>
</li>
<li class="nav-item me-3">
<p v-if="infobar.plays_in" class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
Last song ends in <% infobar.end_personal_queue.secondsToMMSS() %>
</p>
<p v-else class="navbar-text mb-0 start-queue hidden-sm hidden-xs">
Last song ends at <% (infobar.now_in_seconds + infobar.end_personal_queue).timestampToHHMMSS() %>
</p>
<p class="navbar-text mb-0 end-queue"></p>
</li>
<li class="nav-item">
<p class="navbar-text mb-0 duration-queue" v-bind:class="{danger: infobar.length_personal_queue > infobar.max_length * 60}">(<% infobar.length_personal_queue.secondsToMMSS() %>)</p>
<p class="navbar-text mb-0 duration-queue"></p>
</li>
</ul>
</div>
@ -77,7 +66,7 @@
<td class="col-md-2 d-sm-table-cell d-none">Requested By</td>
<td class="col-md-1 text-info d-sm-table-cell d-none" style="cursor: pointer;">
<span v-if="playsIn" id="timeswitch" class="btn btn-link p-0" v-on:click="playsIn = false">Plays In</span>
<span v-else class="btn btn-link p-0" v-on:click="playsIn = true">Plays At</span>
<span v-else class="btn btn-link p-0" v-on:click="playsIn = true">Plays at</span>
</td>
<td class="col-md-1 control-icons">Control</td>
</tr>
@ -190,7 +179,7 @@
<% song.artist %>
</td>
<td>
<button v-on:click="request_song(song.id);" class="btn btn-link p-0 text-decoration-none"><% song.title %></button>
<a href="#" v-on:click="request_song(song.id);"><% song.title %></a>
</td>
<td>
<template v-if="song.user === null">
@ -204,9 +193,9 @@
<% song.duration.secondsToMMSS() %>
</td>
<td>
<button v-on:click="report_song(song.id);" class="btn btn-link p-0 text-decoration-none">
<a href="#" v-on:click="report_song(song.id);">
</button>
</a>
</td>
</tr>
</template>
@ -263,6 +252,27 @@
});
},
computed: {
infoBar() {
let infoBar = {
start_personal_queue: 0,
length_personal_queue: 0,
length_total_queue: 0,
end_personal_queue: 0,
max_length: 45,
}
for (let i = 0; i < this.queue.length; i++) {
const current_song = this.queue[i];
infoBar['length_total_queue'] = infoBar['length_total_queue'] + current_song.song.duration;
if (current_song.user !== null && current_song.user.id === this.user_data.id) {
infoBar['length_personal_queue'] = infoBar['length_personal_queue'] + current_song.song.duration;
infoBar['end_personal_queue'] = infoBar['length_total_queue'];
if (infoBar['start_personal_queue'] === 0) {
infoBar['start_personal_queue'] = infoBar['length_total_queue'] - current_song.song.duration;
}
}
}
return infoBar;
},
play_next_song_at() {
if (this.started_at !== null && this.current_song !== null) {
return this.started_at + this.current_song.song.duration;
@ -290,42 +300,6 @@
}
}
}
this.update_infobar();
},
update_infobar() {
let infoBar = {
start_personal_queue: null,
length_personal_queue: 0,
length_total_queue: 0,
end_personal_queue: 0,
max_length: 45,
plays_in: this.playsIn,
now_in_seconds: 0,
}
const now_in_seconds = Math.round((new Date()).getTime() / 1000);
infoBar.now_in_seconds = now_in_seconds;
let current_song_played = now_in_seconds - this.queue[0].started_at;
// If the current song is the current user's, their queue has started.
if (this.queue[0].user.id == this.user_data.id) {
infoBar.start_personal_queue = 0;
}
for (let i = 0; i < this.queue.length; i++) {
const current_song = this.queue[i];
if (i == 0) {
const current_song_remaining_seconds = current_song.song.duration - this.queue[1].time_until_song_seconds;
infoBar['length_personal_queue'] -= current_song_remaining_seconds;
infoBar['length_total_queue'] -= current_song_remaining_seconds;
}
infoBar['length_total_queue'] += current_song.song.duration;
if (current_song.user !== null && current_song.user.id === this.user_data.id) {
infoBar['length_personal_queue'] += current_song.song.duration;
infoBar['end_personal_queue'] = infoBar['length_total_queue'];
if (infoBar['start_personal_queue'] === null) {
infoBar['start_personal_queue'] = infoBar['length_total_queue'] - current_song.song.duration - this.queue[1].time_until_song_seconds
}
}
}
this.$emit("infobar", infoBar);
},
refresh() {
if (!this.refreshing) {
@ -409,17 +383,7 @@
}).finally(() => {
this.refresh();
});
},
}
});
const personal_queue_vue = new Vue({
el: '#personal-queue-container',
delimiters: ['<%', '%>'],
data: {
infobar: [],
},
mounted() {
queue_vue.$on("infobar", infoBar => this.infobar = infoBar);
}
}
});
</script>
@ -461,7 +425,6 @@
this.page_size = 10;
}
this.page_number = 1;
setCookie("REQUEST_PAGE_SIZE", this.page_size, 14);
this.search();
}
}
@ -473,7 +436,7 @@
},
created() {
fetch(
`/api/v1/songs/?ordering=artist,title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}`
`/api/v1/songs/?ordering=title,artist&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}`
).then(response => {
if (response.status === 200) {
return response.json();
@ -492,15 +455,11 @@
tata.error("", "An unknown error occurred, please try again.")
}
});
const stored_page_size = parseInt(getCookie("REQUEST_PAGE_SIZE"));
if (stored_page_size !== Number.NaN && stored_page_size > 0) {
this.page_size = stored_page_size;
}
},
methods: {
search() {
fetch(
`/api/v1/songs/?ordering=artist,title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}&search=${this.search_input}`,
`/api/v1/songs/?ordering=title,artist&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}&search=${this.search_input}`,
{
headers: {
"X-CSRFToken": CSRF_TOKEN,

View File

@ -1,4 +1,3 @@
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.generics import ListAPIView, RetrieveAPIView, CreateAPIView
from rest_framework import filters
from rest_framework.views import APIView
@ -17,8 +16,7 @@ class SongsListAPIView(ListAPIView):
queryset = Song.objects.all()
permission_classes = [IsAuthenticatedOrTokenHasScopeForMethod]
required_scopes_for_method = {"GET": ["read"]}
filter_backends = (filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend)
filterset_fields = ["user__username", "artist"]
filter_backends = (filters.SearchFilter, filters.OrderingFilter)
search_fields = ["artist", "title", "user__name", "user__username"]
ordering_fields = [
"artist",

View File

@ -1,9 +1,4 @@
import binascii
import socket
import struct
from django.conf import settings
from marietje.utils import send_to_bertha
from queues.models import PlaylistSong
from songs.models import Song
from django.db.models.functions import Coalesce
@ -16,20 +11,6 @@ class UploadException(Exception):
pass
def send_to_bertha(file):
"""Send a file to Berthad file storage."""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(settings.BERTHA_HOST)
sock.sendall(struct.pack("<BQ", 4, file.size))
for chunk in file.chunks():
sock.sendall(chunk)
sock.shutdown(socket.SHUT_WR)
song_hash = binascii.hexlify(sock.recv(64))
sock.close()
return song_hash
def is_regular_queue(ps):
if not ps.played_at:
# Request is from the old times, assume good

View File

@ -74,7 +74,6 @@
typing_timer: null,
page_size: 10,
page_number: 1,
user_data: null,
},
watch: {
search_input: {
@ -102,7 +101,6 @@
this.page_size = 10;
}
this.page_number = 1;
setCookie("MANAGE_PAGE_SIZE", this.page_size, 14);
this.refresh();
}
}
@ -113,23 +111,26 @@
}
},
created() {
fetch('/api/v1/users/me/').then(response => {
fetch(
`/api/v1/songs/?ordering=title,artist&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}`
).then(response => {
if (response.status === 200) {
return response.json();
} else {
throw response;
}
}).then(data => {
this.user_data = data;
}).then(() => {
this.refresh();
}).catch(() => {
tata.error('', 'User details failed to fetch, please reload this page to try again.');
this.songs = data.results;
this.total_songs = data.count;
}).catch((e) => {
if (e instanceof Response) {
e.json().then(data => {
tata.error("", data.errorMessage);
});
} else {
tata.error("", "An unknown error occurred, please try again.")
}
});
const stored_page_size = parseInt(getCookie("MANAGE_PAGE_SIZE"));
if (stored_page_size !== Number.NaN && stored_page_size > 0) {
this.page_size = stored_page_size;
}
},
methods: {
search() {
@ -138,7 +139,7 @@
},
refresh() {
fetch(
`/api/v1/songs/?ordering=artist,title&user__username=${this.user_data.username}&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}&search=${this.search_input}`,
`/api/v1/songs/?ordering=title,artist&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}&search=${this.search_input}`,
{
headers: {
"X-CSRFToken": CSRF_TOKEN,

View File

@ -134,7 +134,7 @@
<th>#</th>
<th>Artist</th>
<th>Title</th>
<th style="white-space: nowrap; text-align: right;"># Requests</th>
<th># Requests</th>
</tr>
</thead>
<tbody>
@ -143,7 +143,7 @@
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__artist }}</td>
<td>{{ stat.song__title }}</td>
<td style="text-align: right;">{{ stat.total }}</td>
<td>{{ stat.total }}</td>
</tr>
{% endfor %}
</tbody>
@ -151,7 +151,7 @@
</div>
</div>
<div class="col-md-6">
<h2>Most played artists</h2>
<h2>Most played Artists</h2>
<p>These are the {{ stats.stats_top_count }} most played artists ever.</p>
<div class="table-responsive">
<table class="table table-striped">
@ -210,7 +210,7 @@
<th>#</th>
<th>Artist</th>
<th>Title</th>
<th style="white-space: nowrap; text-align: right;"># Requests</th>
<th># Requests</th>
</tr>
</thead>
<tbody>
@ -219,7 +219,7 @@
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__artist }}</td>
<td>{{ stat.song__title }}</td>
<td style="text-align: right;">{{ stat.total }}</td>
<td>{{ stat.total }}</td>
</tr>
{% endfor %}
</tbody>

View File

@ -32,7 +32,7 @@
<th>#</th>
<th>Artist</th>
<th>Title</th>
<th style="white-space: nowrap; text-align: right;"># Requests</th>
<th># Requests</th>
</tr>
</thead>
<tbody>
@ -41,7 +41,7 @@
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__artist }}</td>
<td>{{ stat.song__title }}</td>
<td style="text-align: right;">{{ stat.total }}</td>
<td style="text-align: middle;">{{ stat.total }}</td>
</tr>
{% endfor %}
</tbody>
@ -49,7 +49,7 @@
</div>
</div>
<div class="col-md-6">
<h2>Most played artists</h2>
<h2>Most played Artists</h2>
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">

446
poetry.lock generated
View File

@ -120,26 +120,6 @@ d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "bleach"
version = "5.0.1"
description = "An easy safelist-based HTML-sanitizing tool."
optional = false
python-versions = ">=3.7"
files = [
{file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"},
{file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"},
]
[package.dependencies]
six = ">=1.9.0"
tinycss2 = {version = ">=1.1.0,<1.2", optional = true, markers = "extra == \"css\""}
webencodings = "*"
[package.extras]
css = ["tinycss2 (>=1.1.0,<1.2)"]
dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"]
[[package]]
name = "certifi"
version = "2023.7.22"
@ -153,63 +133,75 @@ files = [
[[package]]
name = "cffi"
version = "1.16.0"
version = "1.15.1"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
python-versions = "*"
files = [
{file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
{file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"},
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"},
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"},
{file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"},
{file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"},
{file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
{file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
{file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
{file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
{file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
{file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
{file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
{file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
{file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
{file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"},
{file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"},
{file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"},
{file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"},
{file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"},
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"},
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"},
{file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"},
{file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"},
{file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
{file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
{file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
{file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
{file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
{file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
{file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
{file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
{file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
{file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
{file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
{file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
{file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
{file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
{file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
{file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
{file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
{file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
{file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
{file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
{file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
{file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
{file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
{file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
{file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
{file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
{file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
{file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
{file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
{file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
{file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
{file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
{file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
{file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
{file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
]
[package.dependencies]
@ -217,101 +209,86 @@ pycparser = "*"
[[package]]
name = "charset-normalizer"
version = "3.3.0"
version = "3.2.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
files = [
{file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"},
{file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"},
{file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"},
{file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"},
{file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"},
{file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"},
{file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"},
{file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"},
{file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"},
{file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"},
{file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"},
{file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"},
{file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"},
{file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"},
{file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"},
{file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"},
{file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"},
{file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"},
{file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"},
{file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"},
{file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"},
{file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"},
{file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"},
{file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"},
{file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"},
{file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"},
{file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"},
{file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"},
{file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"},
{file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"},
{file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"},
{file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"},
{file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"},
{file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"},
{file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"},
{file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"},
{file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"},
{file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"},
{file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"},
{file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"},
{file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"},
{file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"},
{file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"},
{file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"},
{file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"},
{file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"},
{file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"},
{file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"},
{file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"},
{file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"},
{file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"},
{file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"},
{file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"},
{file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"},
{file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"},
{file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"},
{file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"},
{file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"},
{file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"},
{file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"},
{file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"},
{file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"},
{file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"},
{file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"},
{file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"},
{file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"},
{file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"},
{file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"},
{file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"},
{file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"},
{file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"},
{file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"},
{file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"},
{file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"},
{file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"},
{file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"},
{file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"},
{file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"},
{file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"},
{file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"},
{file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"},
{file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"},
{file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"},
{file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"},
{file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"},
{file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"},
{file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"},
{file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"},
{file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"},
{file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"},
{file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"},
{file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"},
{file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"},
{file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"},
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"},
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"},
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"},
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"},
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"},
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"},
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"},
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"},
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"},
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"},
{file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"},
{file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"},
{file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"},
{file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"},
{file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"},
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"},
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"},
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"},
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"},
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"},
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"},
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"},
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"},
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"},
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"},
{file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"},
{file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"},
{file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"},
{file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"},
{file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"},
{file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"},
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"},
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"},
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"},
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"},
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"},
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"},
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"},
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"},
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"},
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"},
{file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"},
{file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"},
{file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"},
{file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"},
{file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"},
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"},
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"},
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"},
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"},
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"},
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"},
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"},
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"},
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"},
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"},
{file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"},
{file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"},
{file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"},
]
[[package]]
@ -341,34 +318,34 @@ files = [
[[package]]
name = "cryptography"
version = "41.0.4"
version = "41.0.3"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
{file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839"},
{file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f"},
{file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714"},
{file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb"},
{file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13"},
{file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143"},
{file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397"},
{file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860"},
{file = "cryptography-41.0.4-cp37-abi3-win32.whl", hash = "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd"},
{file = "cryptography-41.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d"},
{file = "cryptography-41.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67"},
{file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e"},
{file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829"},
{file = "cryptography-41.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca"},
{file = "cryptography-41.0.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d"},
{file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac"},
{file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9"},
{file = "cryptography-41.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"},
{file = "cryptography-41.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91"},
{file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8"},
{file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6"},
{file = "cryptography-41.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311"},
{file = "cryptography-41.0.4.tar.gz", hash = "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a"},
{file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"},
{file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"},
{file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"},
{file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"},
{file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"},
{file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"},
{file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"},
{file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"},
{file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"},
{file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"},
{file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"},
{file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"},
{file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"},
{file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"},
{file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"},
{file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"},
{file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"},
{file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"},
{file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"},
{file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"},
{file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"},
{file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"},
{file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"},
]
[package.dependencies]
@ -403,13 +380,13 @@ dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
[[package]]
name = "django"
version = "4.2.6"
version = "4.2.5"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.8"
files = [
{file = "Django-4.2.6-py3-none-any.whl", hash = "sha256:a64d2487cdb00ad7461434320ccc38e60af9c404773a2f95ab0093b4453a3215"},
{file = "Django-4.2.6.tar.gz", hash = "sha256:08f41f468b63335aea0d904c5729e0250300f6a1907bf293a65499496cdbc68f"},
{file = "Django-4.2.5-py3-none-any.whl", hash = "sha256:b6b2b5cae821077f137dc4dade696a1c2aa292f892eca28fa8d7bfdf2608ddd4"},
{file = "Django-4.2.5.tar.gz", hash = "sha256:5e5c1c9548ffb7796b4a8a4782e9a2e5a3df3615259fc1bfd3ebc73b646146c1"},
]
[package.dependencies]
@ -435,20 +412,6 @@ files = [
[package.dependencies]
django = ">=3.2"
[[package]]
name = "django-filter"
version = "23.3"
description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
optional = false
python-versions = ">=3.7"
files = [
{file = "django-filter-23.3.tar.gz", hash = "sha256:015fe155582e1805b40629344e4a6cf3cc40450827d294d040b4b8c1749a9fa6"},
{file = "django_filter-23.3-py3-none-any.whl", hash = "sha256:65bc5d1d8f4fff3aaf74cb5da537b6620e9214fb4b3180f6c560776b1b6dccd0"},
]
[package.dependencies]
Django = ">=3.2"
[[package]]
name = "django-oauth-toolkit"
version = "2.3.0"
@ -466,17 +429,6 @@ jwcrypto = ">=0.8.0"
oauthlib = ">=3.1.0"
requests = ">=2.13.0"
[[package]]
name = "django-tinymce"
version = "3.6.1"
description = "A Django application that contains a widget to render a form field as a TinyMCE editor."
optional = false
python-versions = "*"
files = [
{file = "django-tinymce-3.6.1.tar.gz", hash = "sha256:6f4f6227c2c608052081a436a1e3054c441caae24c9e0c8c3010536e24749e29"},
{file = "django_tinymce-3.6.1-py3-none-any.whl", hash = "sha256:da5732413f51cf854352e3148f06f170b59d95a6c6a43fd9f7ccfdd1849f4bf9"},
]
[[package]]
name = "djangorestframework"
version = "3.14.0"
@ -610,13 +562,13 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "packaging"
version = "23.2"
version = "23.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.7"
files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
{file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
{file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
]
[[package]]
@ -632,13 +584,13 @@ files = [
[[package]]
name = "platformdirs"
version = "3.11.0"
version = "3.10.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
{file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
{file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"},
{file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"},
]
[package.extras]
@ -783,17 +735,6 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "sqlparse"
version = "0.4.4"
@ -810,24 +751,6 @@ dev = ["build", "flake8"]
doc = ["sphinx"]
test = ["pytest", "pytest-cov"]
[[package]]
name = "tinycss2"
version = "1.1.1"
description = "A tiny CSS parser"
optional = false
python-versions = ">=3.6"
files = [
{file = "tinycss2-1.1.1-py3-none-any.whl", hash = "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8"},
{file = "tinycss2-1.1.1.tar.gz", hash = "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf"},
]
[package.dependencies]
webencodings = ">=0.4"
[package.extras]
doc = ["sphinx", "sphinx_rtd_theme"]
test = ["coverage[toml]", "pytest", "pytest-cov", "pytest-flake8", "pytest-isort"]
[[package]]
name = "tomli"
version = "2.0.1"
@ -874,13 +797,13 @@ files = [
[[package]]
name = "urllib3"
version = "2.0.6"
version = "2.0.4"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.7"
files = [
{file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"},
{file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"},
{file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"},
{file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"},
]
[package.extras]
@ -899,17 +822,6 @@ files = [
{file = "uwsgi-2.0.22.tar.gz", hash = "sha256:4cc4727258671ac5fa17ab422155e9aaef8a2008ebb86e4404b66deaae965db2"},
]
[[package]]
name = "webencodings"
version = "0.5.1"
description = "Character encoding aliases for legacy web content"
optional = false
python-versions = "*"
files = [
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
]
[[package]]
name = "wrapt"
version = "1.15.0"
@ -997,4 +909,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "ae2a204f7f9ffa4164c97cab1bb80147d32422816164e5f9101daa11e3746e3e"
content-hash = "06f67dca27bc15e59c5195d91a0817565d8ab6c9d2eb5bd7898b7a3310df91ac"

View File

@ -11,7 +11,7 @@ authors = [
"Lars van Rhijn <l.vanrhijn@student.science.ru.nl>",
]
maintainers = [
"Kees van Kempen <ru@keesvankempen.nl>",
"Kees van Kempen <ru@keesvankempen.nl",
"Lars van Rhijn <l.vanrhijn@student.science.ru.nl>",
]
readme = "README.md"
@ -30,9 +30,6 @@ argon2-cffi = "^23.1.0"
uritemplate = "^4.1.1"
pyyaml = "^6.0.1"
uwsgi = "^2.0.22"
django-filter = "^23.3"
django-tinymce = "^3.6.1"
bleach = { extras = ["css"], version = "^5.0.1" }
[tool.poetry.group.dev.dependencies]
black = "^23.7.0"