Compare commits

..

44 commits

Author SHA1 Message Date
bae5d79ce8
Add Celery configuration to settings and create Docker Compose file for services 2025-06-14 17:04:44 +02:00
fd09bf8aa3
Add Docker support with build and push workflow, and configure Gunicorn for application serving
All checks were successful
Build and push Docker image / build (push) Successful in 2m14s
2025-06-14 16:43:38 +02:00
3d180d3359
Implement asynchronous playlist management for MusikGame creation and deletion 2025-06-14 16:22:25 +02:00
51b44bcec1
Add signal to delete YouTube playlist on MusikGame deletion 2025-06-14 16:03:51 +02:00
562bd3d205
Add loading state to YouTube playlist link in game details 2025-06-14 15:51:45 +02:00
4b331f7968
Refactor form handling and improve group retrieval in game views 2025-06-14 15:46:43 +02:00
04b0a30e76
Enhance UI by updating header styles and improving group detail layout 2025-06-14 15:05:50 +02:00
5446175cad
Fix hero section background gradient positioning for improved responsiveness 2025-06-14 14:39:10 +02:00
9cb4f42d07
Add hero section with background gradient and logo to enhance user interface 2025-06-14 13:49:01 +02:00
3042382ae4
Add YouTube playlist support in game detail views and create custom template tag 2025-06-14 12:53:46 +02:00
08e4d6e75b
Add game start and rename buttons in group detail view 2025-06-14 12:39:55 +02:00
0a930e575c
Add unblacklist functionality for group music and update URLs 2025-06-14 12:39:12 +02:00
5e394a8c03
Refactor group detail view: change button style for clearing blacklist to secondary 2025-06-14 12:34:14 +02:00
5de1c8f520
Refactor group detail view: update blacklist clearing to use POST method and enhance button layout for group actions 2025-06-14 12:33:36 +02:00
637b497362
Enhance group detail view: add conditional buttons for game and member removal based on group ownership 2025-06-14 12:27:24 +02:00
7e54c6e0ad
Implement game removal functionality with user feedback in group views 2025-06-14 12:23:14 +02:00
539abe11a1
Fix queryset filter in GroupDetailView to correctly reference group members 2025-06-14 12:16:39 +02:00
16cd905694
Implement member removal functionality with user feedback in group views 2025-06-14 12:15:18 +02:00
7ed5cfcb83
Refactor group music management: update removal logic and enhance user feedback in group views 2025-06-14 12:09:07 +02:00
094c5c104d
Add playlist loading feature and integrate YouTube API for playlist generation 2025-06-14 11:49:04 +02:00
95969b897b
Update playlist link to direct to specific video; wrap music list in details tag 2025-06-14 11:16:41 +02:00
0859b36f98
Enhance message handling in group views; add user feedback for actions and errors
Fix #4
2025-06-14 11:01:48 +02:00
245a2503e2
Refactor group and music game models to use UniqueConstraint; update form error handling in templates 2025-06-14 10:35:42 +02:00
43ec6aafc4
Update music display in group detail template to expand empty message cell 2025-06-14 10:02:32 +02:00
700ab7ecca
Add user signup form and implement signup view; enhance message display in templates 2025-06-14 10:01:45 +02:00
6ab5748cbc
Add favicon link to base template 2025-06-14 09:31:10 +02:00
20b90f410e
Remove out-of-date README sections 2025-06-14 00:22:04 +02:00
c320015205
Fix condition check for musikgame existence in group detail template; add results section in musikgame detail template; update order range in GameCreateView 2025-06-14 00:19:44 +02:00
78e5be580b
Refactor group detail and musikgame detail templates for improved owner visibility and styling; update CSS for music count display 2025-06-14 00:11:39 +02:00
43ba52f31e
Refactor group display in home template to use articles for better structure and styling 2025-06-13 23:58:24 +02:00
6ada3290c8
Add member management functionality in group views and templates 2025-06-13 23:51:10 +02:00
f211b9af50
Add collapsible section for music list in group detail template 2025-06-13 23:26:54 +02:00
e64bb8b95a
Add functionality to clear blacklisted music videos in GroupClearBlacklistView and update group detail template 2025-06-13 23:24:21 +02:00
c55861c439
Update GroupDetailView to exclude blacklisted music videos and mark music as blacklisted in GameCreateView 2025-06-13 23:20:58 +02:00
dfe312d47d
Add group detail link and implement game removal functionality in views 2025-06-13 23:16:53 +02:00
122ae40570
Add playlist field to MusikGame model and update YouTube credentials handling in views 2025-06-13 23:08:41 +02:00
4b2f695afb
Add YoutubeCredentials model and implement YouTube OAuth login functionality 2025-06-13 22:23:18 +02:00
f7baa91132
Add game management features: create MusikGame model, implement game creation and detail views, and update group detail template 2025-06-13 21:06:23 +02:00
19e6eb32c8
Refactor group detail and music views to use MemberFilterMixin for member access control 2025-06-13 19:11:23 +02:00
4e28311b1c
Refactor music video model and views: rename user to owner, add title field, and implement music management in group detail view 2025-06-13 18:55:50 +02:00
8ed39c78b8
Add GroupAddMembersForm and view for editing group members 2025-06-13 17:32:17 +02:00
ba746c9cae
Add game app with group and music video models, views, and templates 2025-06-13 17:10:56 +02:00
d7545ab43c
Setup basic auth 2025-06-13 15:51:32 +02:00
3294ef2a82
Create base structure for Django project 2025-06-13 14:21:31 +02:00
68 changed files with 2916 additions and 299 deletions

View file

@ -0,0 +1,29 @@
name: Build and push Docker image
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: code.edgarpierre.fr
username: ${{ vars.DOCKER_PUSH_USERNAME }}
password: ${{ secrets.DOCKER_PUSH_PASSWORD }}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: |
code.edgarpierre.fr/${{ github.repository }}:${{ github.ref_name }}
code.edgarpierre.fr/${{ github.repository }}:latest

1
.gitignore vendored
View file

@ -166,4 +166,3 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

22
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,22 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/pycqa/isort
rev: 6.0.1
hooks:
- id: isort
name: isort (python)
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.10
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/djlint/djLint
rev: v1.23.3
hooks:
- id: djlint-django
args: ["--reformat", "--lint", "--quiet"]

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.12

17
Dockerfile Normal file
View file

@ -0,0 +1,17 @@
FROM ghcr.io/astral-sh/uv:debian-slim
ADD . /app
WORKDIR /app
RUN useradd -m -r musik && \
chown -R musik /app
USER musik
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV YOUTUBE_OAUTH_SECRETS="/config/secret.json"
RUN uv sync --locked
CMD ["/app/entrypoint.sh"]

View file

@ -1,43 +1,3 @@
# Musik
Script pour créer un jeu de Musik.
## Installation
Pour utiliser la création automatisée de Playlist youtube, les bibliothèques Python
suivantes doivent être installées :
```
pip install --upgrade google-api-python-client
pip install --upgrade google-auth-oauthlib google-auth-httplib2
```
## Configuration
Pour utiliser l'API Youtube, une clé API est nécessaire. Elle doit être stockée dans le
fichier `secret.json`.
## Utilisation
Créer un dossier `lists` qui contient les listes de musiques pour chaque joueur (une url
youtube ou un identifiant de vidéo par ligne). Le nom des fichiers correspondra au nom
des joueurs.
Lancer le script à l'aide de la commande `python -m musik`.
```
usage: python -m musik [-h] [-a] [-c] [-b] [-n NUMBER] [--lists LISTS] [--blacklists BLACKLISTS] [--results RESULTS]
[-v]
Lancer une partie de Musik
options:
-h, --help show this help message and exit
-a, --no-api Désactiver l'API Youtube ; affiche la liste des liens (default: False)
-c, --no-save-creds Désactiver l'enregistrement de la connexion Youtube (default: False)
-b, --no-blacklist Désactiver le méchanisme de blacklist en lecture et écriture (default: False)
-n NUMBER, --number NUMBER
Modifier le nombre de musiques par joueur (default: 2)
--lists LISTS Sélectionner le dossier contenant les listes de musiques (default: lists)
--blacklists BLACKLISTS
Sélectionner le dossier contenant les blacklist (default: blacklists)
--results RESULTS Sélectionner le dossier pour stocker les résultats (default: results)
-v, --verbose
```
Stocker les fichiers des joueurs absents dans un dossier séparé.

0
base/__init__.py Normal file
View file

6
base/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class BaseConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "base"

11
base/forms.py Normal file
View file

@ -0,0 +1,11 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
class UserSignupForm(UserCreationForm):
email = forms.EmailField()
class Meta:
model = User
fields = ["username", "email"]

View file

118
base/static/css/main.css Normal file
View file

@ -0,0 +1,118 @@
:root {
--pico-font-family-sans-serif: Exo, sans-serif;
--pico-font-weight: 450;
}
img.logo {
border-radius: var(--pico-border-radius);
}
header .container {
display: flex;
align-items: center;
justify-content: space-between;
}
form a[role="button"] {
width: 100%;
}
i.owner {
color: var(--pico-color-amber-200);
}
.template {
display: none;
}
a.group {
text-decoration: none;
}
.group-owner, .group i {
margin-left: .5em;
}
.group-owner {
color: var(--pico-color-zinc-500);
}
.music-count {
font-weight: 900;
color: var(--pico-color-zinc-500);
margin-left: .5em;
}
article.message {
&::before {
margin-right: .5em;
font-family: remixicon;
}
&.debug::before {
content: "\eb06";
color: var(--pico-color-zinc-500);
}
&.info::before {
content: "\ee58";
color: var(--pico-color-indigo-600);
}
&.success::before {
content: "\eb80";
color: var(--pico-color-green-500);
}
&.warning::before {
content: "\eca0";
color: var(--pico-color-amber-200);
}
&.error::before {
content: "\eca0";
color: var(--pico-color-red-500);
}
}
.form-error::before {
margin-right: .5em;
font-family: remixicon;
content: "\eca0";
}
#hero {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(
circle 50vh at calc(100vw - 4rem) 50%,
var(--pico-primary-background),
color-mix(in hsl, var(--pico-primary-background), var(--pico-background-color) 10%) 20%,
color-mix(in hsl, var(--pico-primary-background), var(--pico-background-color) 30%) 40%,
color-mix(in hsl, var(--pico-primary-background) 30%, var(--pico-background-color)) 60%,
color-mix(in hsl, var(--pico-primary-background) 10%, var(--pico-background-color)) 80%,
var(--pico-background-color));
display: grid;
align-items: center;
padding: 4rem;
.big-logo {
font-size: 8rem;
}
h1 {
font-size: 4rem;
}
}
h1,
h2,
h3,
h4,
h5,
h6,
.header-title {
font-weight: 900;
}
i.i {
margin-right: .5em;
}

25
base/static/logo.svg Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256" height="256" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="Gradient" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" style="stop-color:#3584E4;stop-opacity:1" />
<stop offset="100%" style="stop-color:#A51D2D;stop-opacity:1" />
</linearGradient>
<filter id="alpha-to-white">
<feColorMatrix in="SourceGraphic" type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<g id="child-svg"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" height="16px" width="16px"><path d="M18.7134 8.12811L18.4668 8.69379C18.2864 9.10792 17.7136 9.10792 17.5331 8.69379L17.2866 8.12811C16.8471 7.11947 16.0555 6.31641 15.0677 5.87708L14.308 5.53922C13.8973 5.35653 13.8973 4.75881 14.308 4.57612L15.0252 4.25714C16.0384 3.80651 16.8442 2.97373 17.2761 1.93083L17.5293 1.31953C17.7058 0.893489 18.2942 0.893489 18.4706 1.31953L18.7238 1.93083C19.1558 2.97373 19.9616 3.80651 20.9748 4.25714L21.6919 4.57612C22.1027 4.75881 22.1027 5.35653 21.6919 5.53922L20.9323 5.87708C19.9445 6.31641 19.1529 7.11947 18.7134 8.12811ZM7 3H12V6H9V17C9 19.2091 7.20914 21 5 21C2.79086 21 1 19.2091 1 17C1 14.7909 2.79086 13 5 13C5.72857 13 6.41165 13.1948 7 13.5351V3ZM18 13.5351V11H20V17C20 19.2091 18.2091 21 16 21C13.7909 21 12 19.2091 12 17C12 14.7909 13.7909 13 16 13C16.7286 13 17.4117 13.1948 18 13.5351Z" /></svg></g>
</defs>
<rect
width="256"
height="256"
fill="url(#Gradient)"
ry="0"
x="0"
y="0" />
<use xlink:href="#child-svg" filter="url(#alpha-to-white)"
transform="matrix(8,0,0,8,64,64)" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% load form %}
{% block content %}
<h1>Créer un compte</h1>
{% form form %}
{% endblock content %}

73
base/templates/base.html Normal file
View file

@ -0,0 +1,73 @@
{% load static %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Le jeu de Musik">
<meta name="keywords" content="musik, jeu, musique">
<title>
{% block title %}
Musik
{% endblock title %}
</title>
<link rel="icon" href="{% static "logo.svg" %}">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Exo:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.purple.min.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/remixicon@4.6.0/fonts/remixicon.min.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.colors.min.css">
<link rel="stylesheet" href="{% static "css/main.css" %}">
</head>
<body>
{% block body %}
<header>
<div class="container">
<nav>
<ul>
<li>
<a href="{% url 'index' %}">
<i class="ri-music-ai-fill"></i> <span class="header-title">Musik</span>
</a>
</li>
{% if object.group %}
<li>
<a href="{% url 'group_detail' pk=object.group.pk %}">
<i class="ri-group-2-fill"></i> {{ object.group.name }}
</a>
</li>
{% endif %}
</ul>
</nav>
<nav>
<ul>
{% if user.is_authenticated %}
<li>{{ user.username }}</li>
<li>
<form action="{% url 'logout' %}" method="post">
{% csrf_token %}
<input type="submit" value="Se déconnecter" class="logout">
</form>
</li>
{% else %}
<li>
<a href="{% url 'login' %}" role="button">Se connecter <i class="ri-login-box-fill"></i></a>
</li>
{% endif %}
</ul>
</nav>
</div>
</header>
<main class="container">
{% for message in messages %}<article class="message {{ message.tags }}">{{ message }}</article>{% endfor %}
{% block content %}
{% endblock content %}
</main>
{% endblock body %}
</body>
</html>

View file

@ -0,0 +1,18 @@
{% for error in form.non_field_errors %}<article class="message error">{{ error }}</article>{% endfor %}
<form method="post" {% if action %}action="{% url action %}"{% endif %}>
{% csrf_token %}
{% for field in form %}
<fieldset>
{% if field.id_for_label %}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{% else %}
<legend>{{ field.label }}</legend>
{% endif %}
{{ field }}
{% if field.errors %}
<small id="{{ field.errors.field_id }}_error" class="form-error">{{ field.errors|join:", " }}</small>
{% endif %}
</fieldset>
{% endfor %}
<input type="submit" {% if submit %}value="{{ submit }}"{% endif %}>
</form>

10
base/templates/hero.html Normal file
View file

@ -0,0 +1,10 @@
{% load static %}
<div id="hero">
<div>
<i class="ri-music-ai-fill big-logo"></i>
<h1>Musik</h1>
<p>
<a href="{% url "login" %}" role="button"><i class="ri-play-fill"></i> Jouer</a>
</p>
</div>
</div>

11
base/templates/index.html Normal file
View file

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block content %}
{% include "game/home.html" %}
{% endblock content %}
{% block body %}
{% if user.is_authenticated %}
{{ block.super }}
{% else %}
{% include "hero.html" %}
{% endif %}
{% endblock body %}

View file

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% load form %}
{% block content %}
<h1>Connexion</h1>
<p>
<a href="{% url "signup" %}">Créer un compte</a>
</p>
{% form form submit="Se connecter" %}
{% endblock content %}

View file

11
base/templatetags/form.py Normal file
View file

@ -0,0 +1,11 @@
from django import template
register = template.Library()
@register.inclusion_tag("base/form.html")
def form(form, **kwargs):
return kwargs | {
"form": form,
"errors": form.errors,
}

9
base/urls.py Normal file
View file

@ -0,0 +1,9 @@
from django.urls import include, path
from . import views
urlpatterns = [
path("", views.HomePageView.as_view(), name="index"),
path("accounts/signup/", views.SignupView.as_view(), name="signup"),
path("accounts/", include("django.contrib.auth.urls")),
]

17
base/views.py Normal file
View file

@ -0,0 +1,17 @@
from django.contrib.auth.models import User
from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic.base import TemplateView
from django.views.generic.edit import CreateView
from . import forms
class HomePageView(TemplateView):
template_name = "index.html"
class SignupView(SuccessMessageMixin, CreateView):
model = User
form_class = forms.UserSignupForm
success_url = "/"
success_message = "Le compte %(username)s a été créé avec succès."

29
compose.yaml Normal file
View file

@ -0,0 +1,29 @@
services:
musik:
image: code.edgarpierre.fr/edpibu/musik
container_name: musik
restart: unless-stopped
ports:
- 35001:8000
volumes:
- /docker/musik/config:/config
- /docker/musik/static:/app/static
environment:
CELERY_BROKER_URL: amqp://rabbitmq:5672//
depends_on:
- rabbitmq
- celery
celery:
image: code.edgarpierre.fr/edpibu/musik
container_name: musik_celery
restart: unless-stopped
command: uv run celery -A musik worker
environment:
CELERY_BROKER_URL: amqp://rabbitmq:5672//
depends_on:
- rabbitmq
rabbitmq:
image: rabbitmq
container_name: musik_rabbitmq

5
entrypoint.sh Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env sh
cd /app
uv run manage.py collectstatic --noinput
uv run manage.py migrate --noinput
uv run gunicorn --bind :8000 --workers 2 musik.wsgi:application

0
game/__init__.py Normal file
View file

6
game/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class GameConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "game"

31
game/forms.py Normal file
View file

@ -0,0 +1,31 @@
from django import forms
from . import models
class GroupAddMembersForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["members"].queryset = self.fields["members"].queryset.exclude(
id=self.instance.owner.id
)
class Meta:
model = models.Group
fields = ["members"]
class MusikGameForm(forms.ModelForm):
class Meta:
model = models.MusikGame
fields = ["players", "n"]
widgets = {
"players": forms.CheckboxSelectMultiple,
}
def __init__(self, *args, **kwargs):
group = models.Group.objects.get(pk=kwargs.pop("group", None))
players = group.members.all() | models.User.objects.filter(id=group.owner.id)
kwargs["initial"].setdefault("players", players)
super().__init__(*args, **kwargs)
self.fields["players"].queryset = players

View file

@ -0,0 +1,72 @@
# Generated by Django 5.2.3 on 2025-06-13 14:12
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Group",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField()),
(
"members",
models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="owned_group_set",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="MusicVideo",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("yt_id", models.CharField(max_length=16)),
("blacklisted", models.BooleanField(default=False)),
(
"group",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="game.group"
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 5.2.3 on 2025-06-13 15:04
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("game", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="group",
name="name",
field=models.CharField(verbose_name="Nom du groupe"),
),
migrations.AlterUniqueTogether(
name="group",
unique_together={("name", "owner")},
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 5.2.3 on 2025-06-13 16:21
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("game", "0002_alter_group_name_alter_group_unique_together"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RenameField(
model_name="musicvideo",
old_name="user",
new_name="owner",
),
migrations.AlterUniqueTogether(
name="musicvideo",
unique_together={("yt_id", "owner", "group")},
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 5.2.3 on 2025-06-13 16:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("game", "0003_rename_user_musicvideo_owner_and_more"),
]
operations = [
migrations.AddField(
model_name="musicvideo",
name="title",
field=models.CharField(blank=True),
),
]

View file

@ -0,0 +1,47 @@
# Generated by Django 5.2.3 on 2025-06-13 17:23
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("game", "0004_musicvideo_title"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="musicvideo",
name="date_added",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.CreateModel(
name="MusikGame",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("date", models.DateTimeField(auto_now_add=True)),
(
"group",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="game.group"
),
),
("music_videos", models.ManyToManyField(to="game.musicvideo")),
("players", models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 5.2.3 on 2025-06-13 18:41
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("game", "0005_musicvideo_date_added_musikgame"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="musikgame",
name="n",
field=models.PositiveIntegerField(
default=2, verbose_name="Nombre de musiques"
),
),
migrations.AlterField(
model_name="musikgame",
name="players",
field=models.ManyToManyField(
to=settings.AUTH_USER_MODEL, verbose_name="Joueurs"
),
),
]

View file

@ -0,0 +1,61 @@
# Generated by Django 5.2.3 on 2025-06-13 18:57
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("game", "0006_musikgame_n_alter_musikgame_players"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name="musikgame",
name="music_videos",
),
migrations.CreateModel(
name="MusicGameOrder",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("order", models.PositiveIntegerField()),
(
"game",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="game.musikgame"
),
),
(
"music_video",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="game.musicvideo",
),
),
(
"player",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["order"],
"unique_together": {
("game", "order"),
("game", "player", "music_video"),
},
},
),
]

View file

@ -0,0 +1,37 @@
# Generated by Django 5.2.3 on 2025-06-13 20:16
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("game", "0007_remove_musikgame_music_videos_musicgameorder"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="YoutubeCredentials",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("credentials", models.JSONField()),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 5.2.3 on 2025-06-13 20:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("game", "0008_youtubecredentials"),
]
operations = [
migrations.AlterField(
model_name="youtubecredentials",
name="credentials",
field=models.CharField(),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 5.2.3 on 2025-06-13 20:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("game", "0009_alter_youtubecredentials_credentials"),
]
operations = [
migrations.AlterField(
model_name="youtubecredentials",
name="credentials",
field=models.JSONField(),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 5.2.3 on 2025-06-13 21:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("game", "0010_alter_youtubecredentials_credentials"),
]
operations = [
migrations.AddField(
model_name="musikgame",
name="playlist",
field=models.URLField(blank=True, verbose_name="Playlist YouTube"),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 5.2.3 on 2025-06-13 21:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("game", "0011_musikgame_playlist"),
]
operations = [
migrations.AlterField(
model_name="musikgame",
name="playlist",
field=models.CharField(blank=True, verbose_name="Playlist YouTube"),
),
]

View file

@ -0,0 +1,53 @@
# Generated by Django 5.2.3 on 2025-06-14 08:13
import django.db.models.functions.text
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("game", "0012_alter_musikgame_playlist"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterUniqueTogether(
name="group",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="musicgameorder",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="musicvideo",
unique_together=set(),
),
migrations.AddConstraint(
model_name="group",
constraint=models.UniqueConstraint(
django.db.models.functions.text.Lower("name"),
models.F("owner"),
name="unique_group_name",
),
),
migrations.AddConstraint(
model_name="musicgameorder",
constraint=models.UniqueConstraint(
fields=("game", "player", "music_video"), name="unique_music_in_game"
),
),
migrations.AddConstraint(
model_name="musicgameorder",
constraint=models.UniqueConstraint(
fields=("game", "order"), name="unique_order"
),
),
migrations.AddConstraint(
model_name="musicvideo",
constraint=models.UniqueConstraint(
fields=("yt_id", "owner", "group"), name="unique_music_in_group"
),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 5.2.3 on 2025-06-14 09:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("game", "0013_alter_group_unique_together_and_more"),
]
operations = [
migrations.AddField(
model_name="musikgame",
name="playlist_loading",
field=models.BooleanField(default=False),
),
]

View file

91
game/models.py Normal file
View file

@ -0,0 +1,91 @@
from django.contrib.auth.models import User
from django.db import models
from django.db.models.functions import Lower
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.urls import reverse
from . import tasks
class YoutubeCredentials(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
credentials = models.JSONField()
class Group(models.Model):
name = models.CharField(verbose_name="Nom du groupe")
owner = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="owned_group_set"
)
members = models.ManyToManyField(User, blank=True)
def get_absolute_url(self):
return reverse("group_detail", kwargs={"pk": self.pk})
class Meta:
constraints = [
models.UniqueConstraint(Lower("name"), "owner", name="unique_group_name")
]
class MusicVideo(models.Model):
yt_id = models.CharField(max_length=16)
title = models.CharField(blank=True)
date_added = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
blacklisted = models.BooleanField(default=False)
class Meta:
constraints = [
models.UniqueConstraint(
fields=("yt_id", "owner", "group"), name="unique_music_in_group"
)
]
class MusikGame(models.Model):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
date = models.DateTimeField(auto_now_add=True)
n = models.PositiveIntegerField(default=2, verbose_name="Nombre de musiques")
players = models.ManyToManyField(User, verbose_name="Joueurs")
playlist = models.CharField(blank=True, verbose_name="Playlist YouTube")
playlist_loading = models.BooleanField(default=False)
def get_absolute_url(self):
return reverse("game_detail", kwargs={"pk": self.pk})
@receiver(post_save, sender=MusikGame)
def generateYoutubePlaylist(sender, instance, created, **kwargs):
if not instance.playlist_loading or instance.playlist:
return
if creds := instance.group.owner.youtubecredentials:
tasks.generate_playlist.delay_on_commit(creds.credentials, instance.pk)
@receiver(post_delete, sender=MusikGame)
def deleteYoutubePlaylist(sender, instance, using, **kwargs):
if not instance.playlist:
return
if creds := instance.group.owner.youtubecredentials:
tasks.delete_playlist.delay_on_commit(creds.credentials, instance.playlist)
class MusicGameOrder(models.Model):
game = models.ForeignKey(MusikGame, on_delete=models.CASCADE)
player = models.ForeignKey(User, on_delete=models.CASCADE)
music_video = models.ForeignKey(MusicVideo, on_delete=models.CASCADE)
order = models.PositiveIntegerField()
class Meta:
constraints = [
models.UniqueConstraint(
fields=("game", "player", "music_video"), name="unique_music_in_game"
),
models.UniqueConstraint(fields=("game", "order"), name="unique_order"),
]
ordering = ["order"]

54
game/tasks.py Normal file
View file

@ -0,0 +1,54 @@
import google.oauth2.credentials
import googleapiclient.discovery
from celery import shared_task
from . import models
@shared_task
def generate_playlist(creds, game_pk):
game = models.MusikGame.objects.get(pk=game_pk)
credentials = google.oauth2.credentials.Credentials(**creds)
yt_api = googleapiclient.discovery.build("youtube", "v3", credentials=credentials)
pl_request = yt_api.playlists().insert(
part="snippet,status",
body={
"snippet": {
"title": f"Musik {game.group.name} {game.date.strftime('%x')}",
"description": "Playlist générée par Musik",
},
"status": {
"privacyStatus": "private",
},
},
)
pl_response = pl_request.execute()
pl_id = pl_response.get("id")
game.playlist = pl_id
game.save()
for music in game.musicgameorder_set.all():
request = yt_api.playlistItems().insert(
part="snippet",
body={
"snippet": {
"playlistId": pl_id,
"resourceId": {
"kind": "youtube#video",
"videoId": music.music_video.yt_id,
},
}
},
)
request.execute()
game.playlist_loading = False
game.save()
@shared_task
def delete_playlist(creds, playlist_id):
credentials = google.oauth2.credentials.Credentials(**creds)
yt_api = googleapiclient.discovery.build("youtube", "v3", credentials=credentials)
pl_request = yt_api.playlists().delete(id=playlist_id)
pl_request.execute()

View file

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% load form %}
{% block content %}
<dialog open>
<article>
<header>
<p>
<i class="ri-group-2-fill"></i> {{ group.name }}
</p>
</header>
<p>
Confirmer la supression du groupe <strong>{{ group.name }}</strong> ?
</p>
<form method="post">
{% csrf_token %}
<button type="submit">
<i class="ri-delete-bin-fill"></i> Confirmer
</button>
<a href="{{ group.get_absolute_url }}" role="button" class="secondary"><i class="ri-close-line"></i> Annuler</a>
</form>
</article>
</dialog>
{% endblock content %}

View file

@ -0,0 +1,204 @@
{% extends "base.html" %}
{% load form youtube %}
{% block content %}
<h1>
{% if group.owner == user %}
<i class="ri-vip-crown-fill owner"></i>
{% else %}
<i class="ri-group-2-fill"></i>
{% endif %}
{{ group.name }}
</h1>
{% if group.owner == user %}
<p>
<form method="post">
{% csrf_token %}
<p>
<a href="{% url "start_game" pk=group.pk %}" role="button"><i class="ri-play-fill"></i> Jouer</a>
</p>
<div role="group">
<a href="{% url "group_update" pk=group.pk %}"
class="secondary"
role="button"><i class="ri-edit-line"></i> Renommer</a>
<button type="submit"
class="secondary"
formaction="{% url "group_clear_blacklist" pk=group.pk %}">
<i class="ri-history-fill"></i> Effacer la blacklist
</button>
</div>
</form>
</p>
{% endif %}
{% if group.musikgame_set.exists %}
<h2>
<i class="ri-play-circle-fill"></i> Parties
</h2>
<form method="post">
{% csrf_token %}
<table>
<thead>
{% if group.owner == user %}<th></th>{% endif %}
<th>Date</th>
<th>
<i class="ri-youtube-fill"></i> Playlists
</th>
<th>Joueurs</th>
</thead>
<tbody>
{% for game in group.musikgame_set.all %}
<tr>
{% if group.owner == user %}
<td>
<input type="checkbox" name="game" value="{{ game.pk }}">
</td>
{% endif %}
<td>
<a href="{% url "game_detail" pk=game.pk %}">{{ game.date }}</a>
</td>
<td>
{% if game.playlist %}
<a href="{% yt_playlist game %}"
{% if game.playlist_loading %}aria-busy="true"{% endif %}><i class="ri-youtube-fill"></i> Playlist</a>
{% endif %}
</td>
<td>{{ game.players.all|join:", " }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if group.owner == user %}
<button type="submit"
class="secondary"
formaction="{% url "group_remove_game" pk=group.pk %}">
<i class="ri-delete-bin-fill"></i> Supprimer les parties sélectionnées
</button>
{% endif %}
</form>
{% endif %}
<h2>
<i class="ri-group-2-fill"></i> Membres
</h2>
<form method="post">
{% csrf_token %}
<table>
<thead>
<tr>
{% if group.owner == user %}<th></th>{% endif %}
<th>Membre</th>
<th>
<i class="ri-vip-crown-fill"></i>
</th>
<th>
<i class="ri-mv-line"></i>
</th>
</tr>
</thead>
<tbody>
<tr>
{% if group.owner == user %}<td></td>{% endif %}
<td>{{ group.owner }}</td>
<td>
<i class="ri-vip-crown-fill owner"></i>
</td>
<td>{{ owner_count }}</td>
</tr>
{% for member in members.all %}
<tr>
{% if group.owner == user %}
<td>
<input type="checkbox" name="member" value="{{ member.pk }}">
</td>
{% endif %}
<td>{{ member }}</td>
<td></td>
<td>{{ member.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if group.owner == user %}
<button type="submit"
class="secondary"
formaction="{% url "group_remove_member" pk=group.pk %}">
<i class="ri-delete-bin-fill"></i> Supprimer les membres sélectionnés
</button>
{% endif %}
</form>
{% if group.owner == user %}
<form method="post" action="{% url "group_add_member" pk=group.pk %}">
{% csrf_token %}
<fieldset role="group">
<input type="string"
name="username"
id="username"
placeholder="Membre"
required>
<button type="submit">Ajouter</button>
</fieldset>
</form>
{% endif %}
<h2>
<i class="ri-music-2-fill"></i> Mes musiques <span class="music-count">{{ musics.count }}</span>
</h2>
<details>
<summary role="button">
<i class="ri-music-2-fill"></i> Liste des musiques
</summary>
<form method="post">
{% csrf_token %}
<table>
<thead>
<tr>
<th></th>
<th>Musique</th>
<th>ID</th>
<th>
<i class="ri-history-fill"></i>
</th>
</tr>
</thead>
<tbody>
{% for music in musics %}
<tr>
<td>
<input type="checkbox" name="musics" value="{{ music.pk }}">
</td>
<th>{{ music.title }}</th>
<td>
<a href="https://youtu.be/{{ music.yt_id }}">{{ music.yt_id }}</a>
</td>
<td>
<input type="checkbox" disabled {% if music.blacklisted %}checked{% endif %}>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4">Aucune musique.</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if musics %}
<fieldset role="group">
<button type="submit"
formaction="{% url "group_remove_music" pk=group.pk %}"
class="secondary">
<i class="ri-delete-bin-fill"></i> Supprimer
</button>
<button type="submit"
class="secondary"
formaction="{% url "group_unblacklist_music" pk=group.pk %}">
<i class="ri-history-fill"></i> Retirer de la blacklist
</button>
</fieldset>
{% endif %}
</form>
</details>
<form method="post" action="{% url "group_add_music" pk=group.pk %}">
{% csrf_token %}
<fieldset role="group">
<input type="string" name="yt_id" id="yt_id" placeholder="Musique" required>
<button type="submit">Ajouter</button>
</fieldset>
</form>
{% endblock content %}

View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load form %}
{% block content %}
{% if group %}
<h1>
<i class="ri-group-2-fill"></i> {{ group.name }}
</h1>
<p>
<a href="{% url "group_delete" group.pk %}">Supprimer le groupe</a>
</p>
{% else %}
<h1>Créer un groupe</h1>
{% endif %}
{% form form %}
{% endblock content %}

View file

@ -0,0 +1,26 @@
<p>Bienvenue {{ user.username }} !</p>
<h2>
<i class="ri-group-2-fill"></i> Mes groupes
</h2>
<p>
<a href="{% url "group_create" %}" role="button"><i class="ri-add-box-fill"></i> Créer un groupe</a>
{% if not user.youtubecredentials.credentials %}
<a href="{% url "youtube_login" %}" role="button"><i class="ri-youtube-fill"></i> Me connecter au compte Youtube</a>
{% endif %}
</p>
{% if user.owned_group_set.exists or user.group_set.exists %}
{% for group in user.owned_group_set.all %}
<a class="group" href="{{ group.get_absolute_url }}">
<article>
<i class="ri-vip-crown-fill owner i"></i> {{ group.name }}
</article>
</a>
{% endfor %}
{% for group in user.group_set.all %}
<a class="group" href="{{ group.get_absolute_url }}">
<article>
<i class="ri-group-2-fill i"></i> {{ group.name }} <span class="group-owner">{{ group.owner }}</span>
</article>
</a>
{% endfor %}
{% endif %}

View file

@ -0,0 +1,68 @@
{% extends "base.html" %}
{% load youtube %}
{% block content %}
<h1>
<i class="ri-play-circle-fill"></i> {{ musikgame.date }}
</h1>
{% if musikgame.playlist or musikgame.playlist_loading %}
<p>
<a target="_blank"
href="{% yt_playlist musikgame %}"
role="button"
{% if musikgame.playlist_loading %}aria-busy="true"{% endif %}><i class="ri-youtube-fill"></i> Playlist</a>
</p>
{% endif %}
<h2>
<i class="ri-group-2-fill"></i> Joueurs
</h2>
<p>{{ musikgame.players.all|join:", " }}</p>
<h2>
<i class="ri-music-2-fill"></i> Musiques
</h2>
<details>
<summary role="button">
<i class="ri-list-ordered-2"></i> Musiques
</summary>
<ol>
{% for music in musikgame.musicgameorder_set.all %}
<li>
<a href="https://youtu.be/{{ music.music_video.yt_id }}">{{ music.music_video.title }}</a>
</li>
{% endfor %}
</ol>
</details>
<h2>
<i class="ri-list-ordered"></i> Résultats
</h2>
<details>
<summary role="button">
<i class="ri-list-ordered-2"></i> Résultats
</summary>
<table class="striped">
<thead>
<tr>
<th>
<i class="ri-list-ordered-2"></i>
</th>
<th>
<i class="ri-music-2-fill"></i> Musique
</th>
<th>
<i class="ri-user-line"></i> Joueur
</th>
</tr>
</thead>
<tbody>
{% for music in musikgame.musicgameorder_set.all %}
<tr>
<td>{{ music.order }}</td>
<td>
<a href="https://youtu.be/{{ music.music_video.yt_id }}">{{ music.music_video.title }}</a>
</td>
<td>{{ music.player }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</details>
{% endblock content %}

View file

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% load form %}
{% block content %}
<h1>
<i class="ri-group-2-fill"></i> {{ group.name }}
</h1>
{% form form %}
{% endblock content %}

View file

@ -0,0 +1 @@
<a href="{{ url }}">{{ text }}</a>

View file

View file

@ -0,0 +1,8 @@
from django import template
register = template.Library()
@register.simple_tag
def yt_playlist(game):
return f"https://youtube.com/watch?v={game.musicgameorder_set.first().music_video.yt_id}&list={game.playlist}"

59
game/urls.py Normal file
View file

@ -0,0 +1,59 @@
from django.urls import path
from . import views
urlpatterns = [
path("group/create/", views.GroupCreateView.as_view(), name="group_create"),
path(
"group/<int:pk>/update/", views.GroupUpdateView.as_view(), name="group_update"
),
path(
"group/<int:pk>/delete/", views.GroupDeleteView.as_view(), name="group_delete"
),
path("group/<int:pk>/", views.GroupDetailView.as_view(), name="group_detail"),
path(
"group/<int:pk>/add_music/",
views.GroupAddMusicView.as_view(),
name="group_add_music",
),
path(
"group/<int:pk>/add_member/",
views.GroupAddMemberView.as_view(),
name="group_add_member",
),
path(
"group/<int:pk>/remove_music/",
views.GroupRemoveMusicView.as_view(),
name="group_remove_music",
),
path(
"group/<int:pk>/unblacklist_music/",
views.GroupUnblacklistMusicView.as_view(),
name="group_unblacklist_music",
),
path(
"group/<int:pk>/remove_game/",
views.GroupRemoveGameView.as_view(),
name="group_remove_game",
),
path(
"group/<int:pk>/remove_membrer/",
views.GroupRemoveMemberView.as_view(),
name="group_remove_member",
),
path(
"group/<int:pk>/start_game/", views.GameCreateView.as_view(), name="start_game"
),
path("group/game/<int:pk>/", views.GameDetailView.as_view(), name="game_detail"),
path("youtube_login/", views.YoutubeLoginView.as_view(), name="youtube_login"),
path(
"youtube_callback/",
views.YoutubeCallbackView.as_view(),
name="youtube_callback",
),
path(
"group/<int:pk>/clear_blacklist/",
views.GroupClearBlacklistView.as_view(),
name="group_clear_blacklist",
),
]

35
game/utils.py Normal file
View file

@ -0,0 +1,35 @@
from urllib.parse import parse_qs, urlparse
import requests
from django.conf import settings
def parse_musik(raw):
if "youtube.com" in raw:
return parse_qs(urlparse(raw).query).get("v", [None])[0]
elif "youtu.be" in raw:
return urlparse(raw).path[1:]
return raw
def get_yt_title(yt_id):
if not yt_id:
return None
req = requests.get(
"https://www.googleapis.com/youtube/v3/videos",
params={
"part": "snippet",
"id": yt_id,
"key": settings.YOUTUBE_API_KEY,
},
)
if req.status_code != 200:
raise Exception(f"Error fetching YouTube video: {req.status_code} {req.text}")
data = req.json()
if not data.get("items"):
return None
return data["items"][0]["snippet"]["title"]

368
game/views.py Normal file
View file

@ -0,0 +1,368 @@
import random
import google_auth_oauthlib
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.contrib.messages.views import SuccessMessageMixin
from django.db import IntegrityError
from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, redirect
from django.views import View
from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from . import forms, models, utils
class OwnerFilterMixin(LoginRequiredMixin):
def get_queryset(self):
return super().get_queryset().filter(owner=self.request.user)
class MemberFilterMixin(LoginRequiredMixin):
def get_queryset(self):
return (
super()
.get_queryset()
.filter(Q(members=self.request.user) | Q(owner=self.request.user))
.distinct()
)
class GroupMixin:
model = models.Group
fields = ["name"]
class GroupIntegrityMixin:
def form_valid(self, form):
try:
return super().form_valid(form)
except IntegrityError:
form.add_error("name", "Ce nom de groupe existe déjà.")
return super().form_invalid(form)
class GroupCreateView(LoginRequiredMixin, GroupMixin, GroupIntegrityMixin, CreateView):
def form_valid(self, form):
form.instance.owner = self.request.user
return super().form_valid(form)
class GroupUpdateView(OwnerFilterMixin, GroupMixin, GroupIntegrityMixin, UpdateView):
pass
class GroupDeleteView(OwnerFilterMixin, GroupMixin, SuccessMessageMixin, DeleteView):
success_url = "/"
success_message = "Le groupe a été supprimé avec succès."
class GroupDetailView(MemberFilterMixin, GroupMixin, DetailView):
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data["musics"] = data["group"].musicvideo_set.filter(owner=self.request.user)
data["owner_count"] = (
data["group"]
.musicvideo_set.filter(owner=data["group"].owner, blacklisted=False)
.count()
)
data["members"] = data["group"].members.annotate(
count=Count(
"musicvideo",
filter=Q(
musicvideo__group=data["group"], musicvideo__blacklisted=False
),
)
)
return data
class GroupAddMusicView(MemberFilterMixin, SingleObjectMixin, View):
model = models.Group
def post(self, request, pk):
group = self.get_object()
yt_id = request.POST.get("yt_id")
if not yt_id:
messages.add_message(request, messages.ERROR, "Aucun identifiant donné")
return redirect(group)
yt_id = utils.parse_musik(yt_id)
title = utils.get_yt_title(yt_id)
if not title:
messages.add_message(
request, messages.ERROR, f"Vidéo Youtube invalide : {yt_id}"
)
return redirect(group)
try:
group.musicvideo_set.create(yt_id=yt_id, title=title, owner=request.user)
except IntegrityError:
messages.add_message(
request, messages.ERROR, f"Vidéo Youtube déjà ajoutée : {yt_id}"
)
messages.add_message(
request, messages.SUCCESS, f"Vidéo Youtube ajoutée : {yt_id}"
)
return redirect(group)
class GroupAddMemberView(OwnerFilterMixin, SingleObjectMixin, View):
model = models.Group
def post(self, request, pk):
group = self.get_object()
username = request.POST.get("username")
user = User.objects.get(username=username)
if user == group.owner:
messages.add_message(
request, messages.WARNING, f"{user} est le propriétaire du groupe."
)
return redirect(group)
if user in group.members.all():
messages.add_message(
request, messages.WARNING, f"{user} est déjà membre du groupe."
)
group.members.add(user)
return redirect(group)
class GroupRemoveMusicView(MemberFilterMixin, SingleObjectMixin, View):
model = models.Group
def post(self, request, pk):
group = self.get_object()
musics = group.musicvideo_set.filter(
owner=request.user, pk__in=request.POST.getlist("musics")
)
if musics.count() == 0:
messages.add_message(request, messages.INFO, "Aucune musique supprimée.")
return redirect(group)
if musics.count() != len(request.POST.getlist("musics")):
messages.add_message(
request,
messages.WARNING,
"Certaines musiques n'ont pas pu être supprimées.",
)
musics.delete()
else:
musics.delete()
messages.add_message(
request,
messages.SUCCESS,
"Les musiques sélectionnées ont été supprimées.",
)
return redirect(group)
class GroupUnblacklistMusicView(MemberFilterMixin, SingleObjectMixin, View):
model = models.Group
def post(self, request, pk):
group = self.get_object()
musics = group.musicvideo_set.filter(
owner=request.user, pk__in=request.POST.getlist("musics")
)
musics.update(blacklisted=False)
messages.add_message(
request,
messages.SUCCESS,
"Les musiques sélectionnées ont été enlevées de la blacklist.",
)
return redirect(group)
class GroupRemoveMemberView(OwnerFilterMixin, SingleObjectMixin, View):
model = models.Group
def post(self, request, pk):
group = self.get_object()
relations = models.Group.members.through.objects.filter(
group=group, user__id__in=request.POST.getlist("member")
)
if relations.count() == 0:
messages.add_message(request, messages.INFO, "Aucun membre supprimé.")
return redirect(group)
if relations.count() != len(request.POST.getlist("member")):
messages.add_message(
request,
messages.WARNING,
"Certains membres n'ont pas pu être supprimés.",
)
relations.delete()
else:
relations.delete()
messages.add_message(
request,
messages.SUCCESS,
"Les membres sélectionnés ont été supprimés.",
)
return redirect(group)
class GroupRemoveGameView(OwnerFilterMixin, SingleObjectMixin, View):
model = models.Group
def post(self, request, pk):
group = self.get_object()
games = group.musikgame_set.filter(pk__in=request.POST.getlist("game"))
if games.count() == 0:
messages.add_message(request, messages.INFO, "Aucune partie supprimé.")
return redirect(group)
if games.count() != len(request.POST.getlist("game")):
messages.add_message(
request,
messages.WARNING,
"Certaines parties n'ont pas pu être supprimées.",
)
games.delete()
else:
games.delete()
messages.add_message(
request,
messages.SUCCESS,
"Les parties sélectionnées ont été supprimées.",
)
return redirect(group)
class GameCreateView(LoginRequiredMixin, CreateView):
model = models.MusikGame
form_class = forms.MusikGameForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["group"] = self.kwargs["pk"]
return kwargs
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data["group"] = get_object_or_404(
models.Group, owner=self.request.user, pk=self.kwargs["pk"]
)
return data
def form_valid(self, form):
group = get_object_or_404(
models.Group, owner=self.request.user, pk=self.kwargs["pk"]
)
form.instance.group = group
res = super().form_valid(form)
players = []
musics = []
n = form.instance.n
for player in form.instance.players.all():
music_set = player.musicvideo_set.filter(group=group, blacklisted=False)
if music_set.count() < n:
form.instance.delete()
form.add_error("n", f"{player} n'a pas assez de musiques.")
return super().form_invalid(form)
players += n * [player]
musics += random.sample(
list(music_set.all()),
n,
)
pm_list = list(zip(players, musics))
random.shuffle(pm_list)
for (player, music), order in zip(pm_list, range(1, len(pm_list) + 1)):
music.blacklisted = True
music.save()
models.MusicGameOrder.objects.create(
game=form.instance, player=player, music_video=music, order=order
)
if self.request.user.youtubecredentials:
form.instance.playlist_loading = True
form.instance.save()
return res
class GameDetailView(LoginRequiredMixin, DetailView):
model = models.MusikGame
def get_queryset(self):
return (
super()
.get_queryset()
.filter(
Q(group__members=self.request.user) | Q(group__owner=self.request.user)
)
.distinct()
)
class YoutubeLoginView(LoginRequiredMixin, View):
def get(self, request):
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
settings.YOUTUBE_OAUTH_SECRETS,
["https://www.googleapis.com/auth/youtube.force-ssl"],
)
flow.redirect_uri = "https://localhost/youtube_callback/"
auth_url, state = flow.authorization_url(
access_type="offline",
include_granted_scopes="true",
prompt="consent",
)
self.request.session["state"] = state
return redirect(auth_url)
class YoutubeCallbackView(LoginRequiredMixin, View):
def get(self, request):
if err := request.GET.get("error"):
messages.add_message(
request, messages.ERROR, f"Échec de la connexion à Youtube : {err}"
)
return redirect("/")
state = self.request.session.get("state")
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
settings.YOUTUBE_OAUTH_SECRETS,
["https://www.googleapis.com/auth/youtube.force-ssl"],
state=state,
)
flow.redirect_uri = "https://localhost/youtube_callback/"
flow.fetch_token(code=request.GET.get("code"))
credentials = flow.credentials
models.YoutubeCredentials.objects.update_or_create(
user=request.user,
defaults={
"credentials": {
"token": credentials.token,
"refresh_token": credentials.refresh_token,
"token_uri": credentials.token_uri,
"client_id": credentials.client_id,
"client_secret": credentials.client_secret,
"granted_scopes": credentials.granted_scopes,
}
},
)
messages.add_message(request, messages.SUCCESS, "Connexion à Youtube réussie.")
return redirect("/")
class GroupClearBlacklistView(OwnerFilterMixin, SingleObjectMixin, View):
model = models.Group
def post(self, request, pk):
group = self.get_object()
group.musicvideo_set.filter(blacklisted=True).update(blacklisted=False)
messages.add_message(request, messages.SUCCESS, "La blacklist a été effacée.")
return redirect(group)

23
manage.py Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "musik.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

3
musik/__init__.py Normal file
View file

@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ("celery_app",)

View file

@ -1,118 +0,0 @@
import argparse
import logging
from pathlib import Path
from .list import generate_list, write_blacklist, write_results
ROOT_PATH = Path("./lists")
BLACKLIST = Path("./blacklists")
RESULTS = Path("./results")
NUM_MUS = 2
def main():
# Lecture arguments console
parser = argparse.ArgumentParser(
prog="python -m musik",
description="Lancer une partie de Musik",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-a",
"--no-api",
action="store_true",
help="Désactiver l'API Youtube ; affiche la liste des liens",
)
parser.add_argument(
"-c",
"--no-save-creds",
action="store_true",
help="Désactiver l'enregistrement de la connexion Youtube",
)
parser.add_argument(
"-b",
"--no-blacklist",
action="store_true",
help="Désactiver le méchanisme de blacklist en lecture et écriture",
)
parser.add_argument(
"-n",
"--number",
type=int,
default=NUM_MUS,
help="Modifier le nombre de musiques par joueur",
)
parser.add_argument(
"--lists",
type=Path,
default=ROOT_PATH,
help="Sélectionner le dossier contenant les listes de musiques",
)
parser.add_argument(
"--blacklists",
type=Path,
default=BLACKLIST,
help="Sélectionner le dossier contenant les blacklist",
)
parser.add_argument(
"--results",
type=Path,
default=RESULTS,
help="Sélectionner le dossier pour stocker les résultats",
)
parser.add_argument(
"-v",
"--verbose",
action="store_const",
const=logging.DEBUG,
default=logging.INFO,
)
args = parser.parse_args()
if not args.no_api:
from .youtube import create_playlist
# Configuration logs
logger = logging.getLogger("musik")
logging.basicConfig(
level=args.verbose, format="%(asctime)s\t%(levelname)s\t%(message)s"
)
logger.info("Vérification")
if not args.lists.is_dir():
logger.error(f"Le dossier <{args.lists}> n'existe pas.")
return
if not args.blacklists.is_dir():
logger.warning(f"Le dossier <{args.blacklists}> n'existe pas, il va être créé.")
args.blacklists.mkdir()
if not args.results.is_dir():
logger.warning(f"Le dossier <{args.results}> n'existe pas, il va être créé.")
args.results.mkdir()
if args.number < 1:
logger.error("Le nombre de musiques est inférieur à 1.")
return
# Lecture des fichiers musique dans ROOT_PATH
# Faire un dossier différent pour les gens qui ne jouent pas
logger.info("Génération de la liste de musiques")
musik_list = generate_list(args)
if not args.no_api:
create_playlist(musik_list, args)
else:
logger.info("Liste des musiques :")
for _, musik in musik_list:
logger.info(f"# https://www.youtube.com/watch?v={musik}")
# Écriture des résultats
logger.info("Écriture des résultats")
write_results(musik_list, args)
# Écriture de la blacklist
if not args.no_blacklist:
logger.info("Écriture de la blacklist")
write_blacklist(musik_list, args)
if __name__ == "__main__":
main()

16
musik/asgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
ASGI config for musik project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "musik.settings")
application = get_asgi_application()

11
musik/celery.py Normal file
View file

@ -0,0 +1,11 @@
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "musik.settings")
app = Celery("musik")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()

View file

@ -1,72 +0,0 @@
import logging
import random
import sys
from datetime import datetime
from urllib.parse import parse_qs, urlparse
logger = logging.getLogger("musik.list")
def bl_path(bl, user):
return bl.joinpath(user).with_suffix(".txt")
def parse_musik(raw):
if "youtube.com" in raw:
return parse_qs(urlparse(raw).query).get("v", [None])[0]
elif "youtu.be" in raw:
return urlparse(raw).path[1:]
return raw
def generate_list(args):
musik_list = []
user_list = []
for q in args.lists.iterdir():
_u = q.stem
logger.info(f"Musiques de {_u}")
if (not args.no_blacklist) and bl_path(args.blacklists, _u).exists():
logger.debug("Blacklist")
with bl_path(args.blacklists, _u).open("r") as blf:
blacklist = blf.read().splitlines()
else:
blacklist = []
logger.debug("Lecture de la liste")
with q.open() as f:
_raw_musiks = [parse_musik(_musik) for _musik in f.read().splitlines()]
_musiks = list(set(filter(lambda _m: _m not in blacklist, _raw_musiks)))
if len(_musiks) < args.number:
logger.error(
f"{_u} a {len(_musiks)} musique(s) non black-listée(s) "
f"au lieu de {args.number}"
)
sys.exit()
logger.debug("Ajout des musiques à la liste")
musik_list += random.sample(_musiks, args.number)
user_list += [_u] * args.number
# Shuffle musics
logger.info("Classement aléatoire des musiques")
user_musik_list = list(zip(user_list, musik_list))
random.shuffle(user_musik_list)
return user_musik_list
def write_blacklist(musik_list, args):
for user, musik in musik_list:
with bl_path(args.blacklists, user).open("a") as f:
f.write("\n")
f.write(musik)
def write_results(musik_list, args):
with args.results.joinpath(datetime.now().strftime("%Y%m%d %H%M%S")).with_suffix(
".txt"
).open("a") as f:
f.write(f"Résultats {datetime.now()}\n\n")
f.write("\n".join("\t".join(um) for um in musik_list))

135
musik/settings.py Normal file
View file

@ -0,0 +1,135 @@
"""
Django settings for musik project.
Generated by 'django-admin startproject' using Django 5.2.3.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-&z*xu$^w8btr(%1!y#+0a98)l_q*+*6z54611pi678mdpsar_="
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
CSRF_TRUSTED_ORIGINS = ["https://localhost"]
# Application definition
INSTALLED_APPS = [
"base",
"game",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "musik.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "musik.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = "fr-fr"
TIME_ZONE = "Europe/Paris"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = "static/"
STATIC_ROOT = os.getenv("MUSIK_STATIC_ROOT", "./static")
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGOUT_REDIRECT_URL = "/"
LOGIN_REDIRECT_URL = "/"
YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY", "")
YOUTUBE_OAUTH_SECRETS = os.getenv("YOUTUBE_OAUTH_SECRETS", "")
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", None)

25
musik/urls.py Normal file
View file

@ -0,0 +1,25 @@
"""
URL configuration for musik project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("", include("base.urls")),
path("", include("game.urls")),
path("admin/", admin.site.urls),
]

16
musik/wsgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
WSGI config for musik project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "musik.settings")
application = get_wsgi_application()

View file

@ -1,68 +0,0 @@
import logging
import pickle
from datetime import date
from pathlib import Path
import google_auth_oauthlib.flow
import googleapiclient.discovery
import googleapiclient.errors
logger = logging.getLogger("musik.youtube")
def create_playlist(musik_list, args):
pickle_path = Path("./youtube.pickle")
# Connexion à l'API youtube, obtention d'un jeton OAuth
logger.info("Connexion à l'API Youtube")
flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file(
"./secret.json", ["https://www.googleapis.com/auth/youtube.force-ssl"]
)
if (not args.no_save_creds) and pickle_path.is_file():
with pickle_path.open("rb") as f:
credentials = pickle.load(f)
else:
credentials = flow.run_local_server(port=0)
if not args.no_save_creds:
with pickle_path.open("wb") as f:
pickle.dump(credentials, f)
youtube = googleapiclient.discovery.build("youtube", "v3", credentials=credentials)
# Création d'une playlist
logger.info("Création de la playlist")
pl_request = youtube.playlists().insert(
part="snippet,status",
body={
"snippet": {
"title": f"Musik {date.today().strftime('%x')}",
},
"status": {
"privacyStatus": "private",
},
},
)
pl_response = pl_request.execute()
logger.info(
"Playlist créée : "
f"https://www.youtube.com/playlist?list={pl_response['id']}",
)
# Insertion des musiques dans la playlist
logger.info("Insertion des musiques dans la playlist")
for _, musik in musik_list:
logger.debug(musik)
request = youtube.playlistItems().insert(
part="snippet",
body={
"snippet": {
"playlistId": pl_response.get("id"),
"position": 0,
"resourceId": {
"kind": "youtube#video",
"videoId": musik,
},
}
},
)
request.execute()

22
pyproject.toml Normal file
View file

@ -0,0 +1,22 @@
[project]
name = "musik"
version = "0.1.0"
description = "Le jeu de Musik."
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"celery>=5.5.3",
"django>=5.2.3",
"google-api-python-client>=2.172.0",
"google-auth>=2.40.3",
"google-auth-httplib2>=0.2.0",
"google-auth-oauthlib>=1.2.2",
"gunicorn>=23.0.0",
"requests>=2.32.4",
]
[dependency-groups]
dev = [
"djlint>=1.36.4",
"pre-commit>=4.2.0",
]

795
uv.lock generated Normal file
View file

@ -0,0 +1,795 @@
version = 1
revision = 2
requires-python = ">=3.12"
resolution-markers = [
"python_full_version >= '3.13'",
"python_full_version < '3.13'",
]
[[package]]
name = "amqp"
version = "5.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "vine" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" },
]
[[package]]
name = "asgiref"
version = "3.8.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186, upload-time = "2024-03-22T14:39:36.863Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" },
]
[[package]]
name = "billiard"
version = "4.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7c/58/1546c970afcd2a2428b1bfafecf2371d8951cc34b46701bea73f4280989e/billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f", size = 155031, upload-time = "2024-09-21T13:40:22.491Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/da/43b15f28fe5f9e027b41c539abc5469052e9d48fd75f8ff094ba2a0ae767/billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb", size = 86766, upload-time = "2024-09-21T13:40:20.188Z" },
]
[[package]]
name = "cachetools"
version = "5.5.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" },
]
[[package]]
name = "celery"
version = "5.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "billiard" },
{ name = "click" },
{ name = "click-didyoumean" },
{ name = "click-plugins" },
{ name = "click-repl" },
{ name = "kombu" },
{ name = "python-dateutil" },
{ name = "vine" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144, upload-time = "2025-06-01T11:08:12.563Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" },
]
[[package]]
name = "certifi"
version = "2025.4.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
]
[[package]]
name = "cfgv"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]]
name = "click"
version = "8.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
]
[[package]]
name = "click-didyoumean"
version = "0.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
]
sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" },
]
[[package]]
name = "click-plugins"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5f/1d/45434f64ed749540af821fd7e42b8e4d23ac04b1eda7c26613288d6cd8a8/click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", size = 8164, upload-time = "2019-04-04T04:27:04.82Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/da/824b92d9942f4e472702488857914bdd50f73021efea15b4cad9aca8ecef/click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8", size = 7497, upload-time = "2019-04-04T04:27:03.36Z" },
]
[[package]]
name = "click-repl"
version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "prompt-toolkit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "cssbeautifier"
version = "1.15.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "editorconfig" },
{ name = "jsbeautifier" },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f7/01/fdf41c1e5f93d359681976ba10410a04b299d248e28ecce1d4e88588dde4/cssbeautifier-1.15.4.tar.gz", hash = "sha256:9bb08dc3f64c101a01677f128acf01905914cf406baf87434dcde05b74c0acf5", size = 25376, upload-time = "2025-02-27T17:53:51.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/51/ef6c5628e46092f0a54c7cee69acc827adc6b6aab57b55d344fefbdf28f1/cssbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:78c84d5e5378df7d08622bbd0477a1abdbd209680e95480bf22f12d5701efc98", size = 123667, upload-time = "2025-02-27T17:53:43.594Z" },
]
[[package]]
name = "distlib"
version = "0.3.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" },
]
[[package]]
name = "django"
version = "5.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/af/77b403926025dc6f7fd7b31256394d643469418965eb528eab45d0505358/django-5.2.3.tar.gz", hash = "sha256:335213277666ab2c5cac44a792a6d2f3d58eb79a80c14b6b160cd4afc3b75684", size = 10850303, upload-time = "2025-06-10T10:14:05.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/11/7aff961db37e1ea501a2bb663d27a8ce97f3683b9e5b83d3bfead8b86fa4/django-5.2.3-py3-none-any.whl", hash = "sha256:c517a6334e0fd940066aa9467b29401b93c37cec2e61365d663b80922542069d", size = 8301935, upload-time = "2025-06-10T10:13:58.993Z" },
]
[[package]]
name = "djlint"
version = "1.36.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "colorama" },
{ name = "cssbeautifier" },
{ name = "jsbeautifier" },
{ name = "json5" },
{ name = "pathspec" },
{ name = "pyyaml" },
{ name = "regex" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/74/89/ecf5be9f5c59a0c53bcaa29671742c5e269cc7d0e2622e3f65f41df251bf/djlint-1.36.4.tar.gz", hash = "sha256:17254f218b46fe5a714b224c85074c099bcb74e3b2e1f15c2ddc2cf415a408a1", size = 47849, upload-time = "2024-12-24T13:06:36.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/f5/9ae02b875604755d4d00cebf96b218b0faa3198edc630f56a139581aed87/djlint-1.36.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff9faffd7d43ac20467493fa71d5355b5b330a00ade1c4d1e859022f4195223b", size = 354886, upload-time = "2024-12-24T13:06:11.571Z" },
{ url = "https://files.pythonhosted.org/packages/97/51/284443ff2f2a278f61d4ae6ae55eaf820ad9f0fd386d781cdfe91f4de495/djlint-1.36.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:79489e262b5ac23a8dfb7ca37f1eea979674cfc2d2644f7061d95bea12c38f7e", size = 323237, upload-time = "2024-12-24T13:06:13.057Z" },
{ url = "https://files.pythonhosted.org/packages/6d/5e/791f4c5571f3f168ad26fa3757af8f7a05c623fde1134a9c4de814ee33b7/djlint-1.36.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e58c5fa8c6477144a0be0a87273706a059e6dd0d6efae01146ae8c29cdfca675", size = 411719, upload-time = "2024-12-24T13:06:15.672Z" },
{ url = "https://files.pythonhosted.org/packages/1f/11/894425add6f84deffcc6e373f2ce250f2f7b01aa58c7f230016ebe7a0085/djlint-1.36.4-cp312-cp312-win_amd64.whl", hash = "sha256:bb6903777bf3124f5efedcddf1f4716aef097a7ec4223fc0fa54b865829a6e08", size = 362076, upload-time = "2024-12-24T13:06:17.517Z" },
{ url = "https://files.pythonhosted.org/packages/da/83/88b4c885812921739f5529a29085c3762705154d41caf7eb9a8886a3380c/djlint-1.36.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ead475013bcac46095b1bbc8cf97ed2f06e83422335734363f8a76b4ba7e47c2", size = 354384, upload-time = "2024-12-24T13:06:20.809Z" },
{ url = "https://files.pythonhosted.org/packages/32/38/67695f7a150b3d9d62fadb65242213d96024151570c3cf5d966effa68b0e/djlint-1.36.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c601dfa68ea253311deb4a29a7362b7a64933bdfcfb5a06618f3e70ad1fa835", size = 322971, upload-time = "2024-12-24T13:06:22.185Z" },
{ url = "https://files.pythonhosted.org/packages/ac/7a/cd851393291b12e7fe17cf5d4d8874b8ea133aebbe9235f5314aabc96a52/djlint-1.36.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda5014f295002363381969864addeb2db13955f1b26e772657c3b273ed7809f", size = 410972, upload-time = "2024-12-24T13:06:24.077Z" },
{ url = "https://files.pythonhosted.org/packages/6c/31/56469120394b970d4f079a552fde21ed27702ca729595ab0ed459eb6d240/djlint-1.36.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ce37e085afe5a30953b2bd87cbe34c37843d94c701fc68a2dda06c1e428ff4", size = 362053, upload-time = "2024-12-24T13:06:25.432Z" },
{ url = "https://files.pythonhosted.org/packages/4b/67/f7aeea9be6fb3bd984487af8d0d80225a0b1e5f6f7126e3332d349fb13fe/djlint-1.36.4-py3-none-any.whl", hash = "sha256:e9699b8ac3057a6ed04fb90835b89bee954ed1959c01541ce4f8f729c938afdd", size = 52290, upload-time = "2024-12-24T13:06:33.76Z" },
]
[[package]]
name = "editorconfig"
version = "0.17.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/88/3a/a61d9a1f319a186b05d14df17daea42fcddea63c213bcd61a929fb3a6796/editorconfig-0.17.1.tar.gz", hash = "sha256:23c08b00e8e08cc3adcddb825251c497478df1dada6aefeb01e626ad37303745", size = 14695, upload-time = "2025-06-09T08:21:37.097Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl", hash = "sha256:1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82", size = 16360, upload-time = "2025-06-09T08:21:35.654Z" },
]
[[package]]
name = "filelock"
version = "3.18.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
]
[[package]]
name = "google-api-core"
version = "2.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-auth" },
{ name = "googleapis-common-protos" },
{ name = "proto-plus" },
{ name = "protobuf" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/21/e9d043e88222317afdbdb567165fdbc3b0aad90064c7e0c9eb0ad9955ad8/google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8", size = 165443, upload-time = "2025-06-12T20:52:20.439Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/4b/ead00905132820b623732b175d66354e9d3e69fcf2a5dcdab780664e7896/google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7", size = 160807, upload-time = "2025-06-12T20:52:19.334Z" },
]
[[package]]
name = "google-api-python-client"
version = "2.172.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core" },
{ name = "google-auth" },
{ name = "google-auth-httplib2" },
{ name = "httplib2" },
{ name = "uritemplate" },
]
sdist = { url = "https://files.pythonhosted.org/packages/02/69/c0cec6be5878d4de161f64096edb3d4a2d1a838f036b8425ea8358d0dfb3/google_api_python_client-2.172.0.tar.gz", hash = "sha256:dcb3b7e067154b2aa41f1776cf86584a5739c0ac74e6ff46fc665790dca0e6a6", size = 13074841, upload-time = "2025-06-10T16:58:41.181Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/fc/8850ccf21c5df43faeaf8bba8c4149ee880b41b8dc7066e3259bcfd921ca/google_api_python_client-2.172.0-py3-none-any.whl", hash = "sha256:9f1b9a268d5dc1228207d246c673d3a09ee211b41a11521d38d9212aeaa43af7", size = 13595800, upload-time = "2025-06-10T16:58:38.143Z" },
]
[[package]]
name = "google-auth"
version = "2.40.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cachetools" },
{ name = "pyasn1-modules" },
{ name = "rsa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" },
]
[[package]]
name = "google-auth-httplib2"
version = "0.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-auth" },
{ name = "httplib2" },
]
sdist = { url = "https://files.pythonhosted.org/packages/56/be/217a598a818567b28e859ff087f347475c807a5649296fb5a817c58dacef/google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", size = 10842, upload-time = "2023-12-12T17:40:30.722Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/8a/fe34d2f3f9470a27b01c9e76226965863f153d5fbe276f83608562e49c04/google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d", size = 9253, upload-time = "2023-12-12T17:40:13.055Z" },
]
[[package]]
name = "google-auth-oauthlib"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-auth" },
{ name = "requests-oauthlib" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fb/87/e10bf24f7bcffc1421b84d6f9c3377c30ec305d082cd737ddaa6d8f77f7c/google_auth_oauthlib-1.2.2.tar.gz", hash = "sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684", size = 20955, upload-time = "2025-04-22T16:40:29.172Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/84/40ee070be95771acd2f4418981edb834979424565c3eec3cd88b6aa09d24/google_auth_oauthlib-1.2.2-py3-none-any.whl", hash = "sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2", size = 19072, upload-time = "2025-04-22T16:40:28.174Z" },
]
[[package]]
name = "googleapis-common-protos"
version = "1.70.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903, upload-time = "2025-04-14T10:17:02.924Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530, upload-time = "2025-04-14T10:17:01.271Z" },
]
[[package]]
name = "gunicorn"
version = "23.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
]
[[package]]
name = "httplib2"
version = "0.22.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyparsing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/ad/2371116b22d616c194aa25ec410c9c6c37f23599dcd590502b74db197584/httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81", size = 351116, upload-time = "2023-03-21T22:29:37.214Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/6c/d2fbdaaa5959339d53ba38e94c123e4e84b8fbc4b84beb0e70d7c1608486/httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc", size = 96854, upload-time = "2023-03-21T22:29:35.683Z" },
]
[[package]]
name = "identify"
version = "2.6.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "jsbeautifier"
version = "1.15.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "editorconfig" },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ea/98/d6cadf4d5a1c03b2136837a435682418c29fdeb66be137128544cecc5b7a/jsbeautifier-1.15.4.tar.gz", hash = "sha256:5bb18d9efb9331d825735fbc5360ee8f1aac5e52780042803943aa7f854f7592", size = 75257, upload-time = "2025-02-27T17:53:53.252Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/14/1c65fccf8413d5f5c6e8425f84675169654395098000d8bddc4e9d3390e1/jsbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:72f65de312a3f10900d7685557f84cb61a9733c50dcc27271a39f5b0051bf528", size = 94707, upload-time = "2025-02-27T17:53:46.152Z" },
]
[[package]]
name = "json5"
version = "0.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/12/be/c6c745ec4c4539b25a278b70e29793f10382947df0d9efba2fa09120895d/json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a", size = 51907, upload-time = "2025-04-03T16:33:13.201Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/9f/3500910d5a98549e3098807493851eeef2b89cdd3032227558a104dfe926/json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db", size = 36079, upload-time = "2025-04-03T16:33:11.927Z" },
]
[[package]]
name = "kombu"
version = "5.5.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "amqp" },
{ name = "packaging" },
{ name = "tzdata" },
{ name = "vine" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992, upload-time = "2025-06-01T10:19:22.281Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034, upload-time = "2025-06-01T10:19:20.436Z" },
]
[[package]]
name = "musik"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "celery" },
{ name = "django" },
{ name = "google-api-python-client" },
{ name = "google-auth" },
{ name = "google-auth-httplib2" },
{ name = "google-auth-oauthlib" },
{ name = "gunicorn" },
{ name = "requests" },
]
[package.dev-dependencies]
dev = [
{ name = "djlint" },
{ name = "pre-commit" },
]
[package.metadata]
requires-dist = [
{ name = "celery", specifier = ">=5.5.3" },
{ name = "django", specifier = ">=5.2.3" },
{ name = "google-api-python-client", specifier = ">=2.172.0" },
{ name = "google-auth", specifier = ">=2.40.3" },
{ name = "google-auth-httplib2", specifier = ">=0.2.0" },
{ name = "google-auth-oauthlib", specifier = ">=1.2.2" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "requests", specifier = ">=2.32.4" },
]
[package.metadata.requires-dev]
dev = [
{ name = "djlint", specifier = ">=1.36.4" },
{ name = "pre-commit", specifier = ">=4.2.0" },
]
[[package]]
name = "nodeenv"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
]
[[package]]
name = "oauthlib"
version = "3.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352, upload-time = "2022-10-17T20:04:27.471Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688, upload-time = "2022-10-17T20:04:24.037Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
[[package]]
name = "platformdirs"
version = "4.3.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
]
[[package]]
name = "pre-commit"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cfgv" },
{ name = "identify" },
{ name = "nodeenv" },
{ name = "pyyaml" },
{ name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
]
[[package]]
name = "prompt-toolkit"
version = "3.0.51"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wcwidth" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" },
]
[[package]]
name = "proto-plus"
version = "1.26.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" },
]
[[package]]
name = "protobuf"
version = "6.31.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" },
{ url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604, upload-time = "2025-05-28T19:25:45.702Z" },
{ url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" },
{ url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" },
]
[[package]]
name = "pyasn1"
version = "0.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
]
[[package]]
name = "pyasn1-modules"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
]
[[package]]
name = "pyparsing"
version = "3.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
]
[[package]]
name = "regex"
version = "2024.11.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781, upload-time = "2024-11-06T20:10:07.07Z" },
{ url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455, upload-time = "2024-11-06T20:10:09.117Z" },
{ url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759, upload-time = "2024-11-06T20:10:11.155Z" },
{ url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976, upload-time = "2024-11-06T20:10:13.24Z" },
{ url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077, upload-time = "2024-11-06T20:10:15.37Z" },
{ url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160, upload-time = "2024-11-06T20:10:19.027Z" },
{ url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896, upload-time = "2024-11-06T20:10:21.85Z" },
{ url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997, upload-time = "2024-11-06T20:10:24.329Z" },
{ url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725, upload-time = "2024-11-06T20:10:28.067Z" },
{ url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481, upload-time = "2024-11-06T20:10:31.612Z" },
{ url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896, upload-time = "2024-11-06T20:10:34.054Z" },
{ url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138, upload-time = "2024-11-06T20:10:36.142Z" },
{ url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692, upload-time = "2024-11-06T20:10:38.394Z" },
{ url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135, upload-time = "2024-11-06T20:10:40.367Z" },
{ url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567, upload-time = "2024-11-06T20:10:43.467Z" },
{ url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525, upload-time = "2024-11-06T20:10:45.19Z" },
{ url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324, upload-time = "2024-11-06T20:10:47.177Z" },
{ url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617, upload-time = "2024-11-06T20:10:49.312Z" },
{ url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023, upload-time = "2024-11-06T20:10:51.102Z" },
{ url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072, upload-time = "2024-11-06T20:10:52.926Z" },
{ url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130, upload-time = "2024-11-06T20:10:54.828Z" },
{ url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857, upload-time = "2024-11-06T20:10:56.634Z" },
{ url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006, upload-time = "2024-11-06T20:10:59.369Z" },
{ url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650, upload-time = "2024-11-06T20:11:02.042Z" },
{ url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545, upload-time = "2024-11-06T20:11:03.933Z" },
{ url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045, upload-time = "2024-11-06T20:11:06.497Z" },
{ url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182, upload-time = "2024-11-06T20:11:09.06Z" },
{ url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733, upload-time = "2024-11-06T20:11:11.256Z" },
{ url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122, upload-time = "2024-11-06T20:11:13.161Z" },
{ url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545, upload-time = "2024-11-06T20:11:15Z" },
]
[[package]]
name = "requests"
version = "2.32.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
]
[[package]]
name = "requests-oauthlib"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "oauthlib" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
]
[[package]]
name = "rsa"
version = "4.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "sqlparse"
version = "0.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
]
[[package]]
name = "tqdm"
version = "4.67.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]
[[package]]
name = "uritemplate"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" },
]
[[package]]
name = "urllib3"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
]
[[package]]
name = "vine"
version = "5.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" },
]
[[package]]
name = "virtualenv"
version = "20.31.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
{ name = "filelock" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" },
]
[[package]]
name = "wcwidth"
version = "0.2.13"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" },
]