diff --git a/.gitignore b/.gitignore index 46d7a914..5979da9e 100755 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ pgbackups # static static +media # cache .pytest_cache @@ -35,3 +36,4 @@ static # settings config.yml docker-compose.override.yml +admin_panel/admin_panel/settings/production.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d87285ec..bdcb743d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,20 +4,18 @@ files: .*\.py$ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace exclude: settings\.py - id: end-of-file-fixer - id: check-added-large-files files: "" - - id: debug-statements - files: .*\.py$ - id: mixed-line-ending args: - --fix=lf - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.8.0 + rev: 24.10.0 hooks: - id: black - repo: https://github.com/pycqa/isort @@ -29,9 +27,10 @@ repos: - black - --filter-files - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.335 + rev: v1.1.390 hooks: - id: pyright + language_version: python3.13 additional_dependencies: - discord - cachetools @@ -39,9 +38,17 @@ repos: - Pillow - prometheus_client - tortoise-orm - - git+https://github.com/fastapi-admin/fastapi-admin.git - aerich==0.6.3 - redis + - django + - dj_database_url + - django-stubs + - django-debug-toolbar + - django-nonrelated-inlines + - social-auth-app-django + - django-admin-autocomplete-filter + - django_admin_action_forms + - django_admin_inline_paginator - repo: https://github.com/csachs/pyproject-flake8 rev: v7.0.0 hooks: diff --git a/Dockerfile b/Dockerfile index 2d85eaa9..a7e45cda 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,11 +14,9 @@ RUN pip install poetry==2.0.1 WORKDIR /code COPY poetry.lock pyproject.toml /code/ -RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi - COPY . /code -RUN mkdir -p /code/static -RUN mkdir -p /code/static/uploads + +RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi # wait for postgres to be ready CMD sleep 2 diff --git a/static/uploads/.gitkeep b/admin_panel/admin_panel/__init__.py similarity index 100% rename from static/uploads/.gitkeep rename to admin_panel/admin_panel/__init__.py diff --git a/admin_panel/admin_panel/admin.py b/admin_panel/admin_panel/admin.py new file mode 100644 index 00000000..f562d8ca --- /dev/null +++ b/admin_panel/admin_panel/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + + +class BallsdexAdminSite(admin.AdminSite): + site_header = "Ballsdex administration" # TODO: use configured bot name + site_title = "Ballsdex admin panel" + site_url = None + final_catch_all_view = False diff --git a/admin_panel/admin_panel/apps.py b/admin_panel/admin_panel/apps.py new file mode 100644 index 00000000..e35a2582 --- /dev/null +++ b/admin_panel/admin_panel/apps.py @@ -0,0 +1,5 @@ +from django.contrib.admin.apps import AdminConfig + + +class BallsdexAdminConfig(AdminConfig): + default_site = "admin_panel.admin.BallsdexAdminSite" diff --git a/admin_panel/admin_panel/asgi.py b/admin_panel/admin_panel/asgi.py new file mode 100644 index 00000000..19f22990 --- /dev/null +++ b/admin_panel/admin_panel/asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "admin_panel.settings") + +application = get_asgi_application() diff --git a/admin_panel/admin_panel/pipeline.py b/admin_panel/admin_panel/pipeline.py new file mode 100644 index 00000000..565b8aa1 --- /dev/null +++ b/admin_panel/admin_panel/pipeline.py @@ -0,0 +1,218 @@ +from enum import Enum +from typing import TYPE_CHECKING, Literal + +import aiohttp +from asgiref.sync import async_to_sync, sync_to_async +from bd_models.models import ( + Ball, + BallInstance, + BlacklistedGuild, + BlacklistedID, + BlacklistHistory, + Block, + Economy, + Friendship, + GuildConfig, + Player, + Regime, + Special, + Trade, + TradeObject, +) +from django.contrib import messages +from django.contrib.auth.models import Group, Permission +from django.contrib.contenttypes.models import ContentType + +from ballsdex.settings import settings + +from .webhook import notify_admins + +if TYPE_CHECKING: + from django.contrib.auth.models import User + from django.db.models import Model + from django.http import HttpRequest + from social_core.backends.base import BaseAuth + +type perm_dict = dict[ + "type[Model]", list[Literal["view", "add", "change", "delete"]] | Literal["*"] +] + +DISCORD_API = "https://discord.com/api/v10/" + + +class Status(Enum): + STAFF = 0 # has a role in the "admin-role-ids" of config.yml + ADMIN = 1 # has a role in the "root-role-ids" of config.yml + TEAM_MEMBER = 2 # is a member of the Discord team owning the application + CO_OWNER = 3 # has its ID in the "co-owners" section of config.yml + OWNER = 4 # owns the application + + +async def get_permissions(permissions: perm_dict) -> list[Permission]: + """ + Returns the list of permissions objects from a dictionnary mapping models to permission codes. + """ + result: list[Permission] = [] + for model, perms in permissions.items(): + content_type = await sync_to_async(ContentType.objects.get_for_model)(model) + if perms == "*": + perms = ["add", "change", "delete", "view"] + for perm in perms: + result.append( + await Permission.objects.aget( + content_type=content_type, codename=f"{perm}_{model._meta.model_name}" + ) + ) + return result + + +async def assign_status(request: "HttpRequest", response: dict, user: "User", status: Status): + """ + Assign the correct attributes and groups to the user based on the given status. + A message will be displayed to the user. + """ + notify = not user.is_staff + + user.is_staff = True + if status == Status.STAFF: + user.is_superuser = False + group, created = await Group.objects.aget_or_create(name="Staff") + if created: + perms: perm_dict = { + BallInstance: ["view"], + BlacklistedGuild: "*", + BlacklistedID: "*", + BlacklistHistory: ["view"], + Block: "*", + Friendship: "*", + GuildConfig: ["view", "change"], + Player: ["view", "change"], + Trade: ["view"], + TradeObject: ["view"], + } + await group.permissions.aadd(*await get_permissions(perms)) + await user.groups.aadd(group) + message = "You were assigned the Staff status because of your Discord roles." + elif status == Status.ADMIN: + user.is_superuser = False + group, created = await Group.objects.aget_or_create(name="Admin") + if created: + perms: perm_dict = { + Ball: "*", + Regime: "*", + Economy: "*", + Special: "*", + BallInstance: "*", + BlacklistedGuild: "*", + BlacklistedID: "*", + BlacklistHistory: ["view"], + Block: "*", + Friendship: "*", + GuildConfig: "*", + Player: "*", + Trade: ["view"], + TradeObject: ["view"], + } + await group.permissions.aadd(*await get_permissions(perms)) + await user.groups.aadd(group) + message = "You were assigned the Admin status because of your Discord roles." + elif status == Status.TEAM_MEMBER: + user.is_superuser = True + message = ( + "You were assigned the superuser status because you are a team member, " + "and the bot is configured to treat team members as owners." + ) + elif status == Status.CO_OWNER: + user.is_superuser = True + message = "You were assigned the superuser status because you are a co-owner in config.yml" + elif status == Status.OWNER: + user.is_superuser = True + message = ( + "You were assigned the superuser status because you are the owner of the application." + ) + else: + raise ValueError(f"Unknown status: {status}") + await user.asave() + + if notify: + messages.success(request, message) + await notify_admins( + f"{response['global_name']} (`{response['username']}`, {response['id']}) has been " + f"assigned the {status.name} status on the admin panel." + ) + + +@async_to_sync +async def configure_status( + request: "HttpRequest", backend: "BaseAuth", user: "User", uid: str, response: dict, **kwargs +): + if backend.name != "discord": + return + if response["mfa_enabled"] is False: + messages.error( + request, "You cannot use an account without multi-factor authentication enabled." + ) + return + discord_id = int(uid) + + # check if user is a co-owner in config.yml (no API call required) + if settings.co_owners and discord_id in settings.co_owners: + await assign_status(request, response, user, Status.CO_OWNER) + return + + headers = {"Authorization": f"Bot {settings.bot_token}"} + async with aiohttp.ClientSession( + base_url=DISCORD_API, headers=headers, raise_for_status=True + ) as session: + + # check if user owns the application, or is part of the team and team members are co owners + async with session.get("applications/@me") as resp: + info = await resp.json() + if info["owner"]["id"] == uid: + await assign_status(request, response, user, Status.OWNER) + return + if ( + settings.team_owners + and info["team"] + and uid in (x["user"]["id"] for x in info["team"]["members"]) + ): + await assign_status(request, response, user, Status.TEAM_MEMBER) + return + + # no admin guild configured, no roles, nothing to do + if not settings.admin_guild_ids or not (settings.admin_role_ids or settings.root_role_ids): + return + + # check if the user owns roles configured as root/admin in config.yml + session.headers["Authorization"] = f"Bearer {response['access_token']}" + async with session.get("users/@me/guilds") as resp: + guilds = await resp.json() + + for guild in guilds: + if int(guild["id"]) not in settings.admin_guild_ids: + continue + async with session.get(f"users/@me/guilds/{guild['id']}/member") as resp: + member = await resp.json() + + # If we find the user with an "admin" role, we must keep iterating in case a "root" + # role is found later. If a "root" role is found, we can immediately stop and assign + is_staff = False + for role in member["roles"]: + if settings.root_role_ids and int(role) in settings.root_role_ids: + await assign_status(request, response, user, Status.ADMIN) + return + elif settings.admin_role_ids and int(role) in settings.admin_role_ids: + is_staff = True + if is_staff: + await assign_status(request, response, user, Status.STAFF) + return + + # If we reached this point, the user has no administration role. + # A user object will have been created, but without is_staff, the admin panel will be blocked. + # It could also be an ex-staff member logging in, which must be handled manually + if user.is_staff or user.is_superuser: + await notify_admins( + f"{response['global_name']} (`{response['username']}`, {response['id']}) logged in to " + "the admin panel using Discord OAuth2, but no staff status has been found. " + f"{user.is_staff=} {user.is_superuser=}" + ) diff --git a/admin_panel/admin_panel/settings/__init__.py b/admin_panel/admin_panel/settings/__init__.py new file mode 100644 index 00000000..8f607e4e --- /dev/null +++ b/admin_panel/admin_panel/settings/__init__.py @@ -0,0 +1 @@ +from .local import * diff --git a/admin_panel/admin_panel/settings/base.py b/admin_panel/admin_panel/settings/base.py new file mode 100644 index 00000000..62557c0e --- /dev/null +++ b/admin_panel/admin_panel/settings/base.py @@ -0,0 +1,159 @@ +# WARNING: DO NOT EDIT THIS FILE DIRECTLY +# This file may be modified with future updates, changing it would break it +# You should copy the production.example.py file as "production.py" and place your settings there +# That file will not be tracked by git + +from pathlib import Path + +import dj_database_url + +from ballsdex.settings import read_settings, settings + +read_settings(Path("../config.yml")) + +BASE_DIR = Path(__file__).resolve().parent.parent + + +SECRET_KEY = None + +ALLOWED_HOSTS = [ + "localhost", + "127.0.0.1", +] +INTERNAL_IPS = [ + "127.0.0.1", +] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "admin_auto_filters", + "django_admin_action_forms", + "django_admin_inline_paginator", + "social_django", + "admin_panel.apps.BallsdexAdminConfig", + "bd_models", + "preview", +] + +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 = "admin_panel.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": ["templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "social_django.context_processors.backends", + "social_django.context_processors.login_redirect", + ], + }, + }, +] + +WSGI_APPLICATION = "admin_panel.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = {"default": dj_database_url.config("BALLSDEXBOT_DB_URL")} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/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.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = "static/" +STATIC_ROOT = "static" +STATICFILES_DIRS = ["staticfiles"] + +MEDIA_URL = "media/" +MEDIA_ROOT = "media" + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +DISCORD_WEBHOOK_URL = None + +# Django social auth settings +SOCIAL_AUTH_JSONFIELD_ENABLED = True +SOCIAL_AUTH_PIPELINE = ( + "social_core.pipeline.social_auth.social_details", + "social_core.pipeline.social_auth.social_uid", + "social_core.pipeline.social_auth.social_user", + "social_core.pipeline.user.get_username", + "social_core.pipeline.user.create_user", + "social_core.pipeline.social_auth.associate_user", + "social_core.pipeline.social_auth.load_extra_data", + "social_core.pipeline.user.user_details", + "admin_panel.pipeline.configure_status", +) +SOCIAL_AUTH_LOGIN_REDIRECT_URL = "/" + +SOCIAL_AUTH_DISCORD_KEY = settings.client_id +SOCIAL_AUTH_DISCORD_SECRET = settings.client_secret +SOCIAL_AUTH_DISCORD_SCOPE = ["identify", "guilds", "guilds.members.read"] + +if settings.client_id and settings.client_secret: + AUTHENTICATION_BACKENDS = [ + "social_core.backends.discord.DiscordOAuth2", + "django.contrib.auth.backends.ModelBackend", + ] +else: + AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + ] diff --git a/admin_panel/admin_panel/settings/dev.py b/admin_panel/admin_panel/settings/dev.py new file mode 100644 index 00000000..f381f32b --- /dev/null +++ b/admin_panel/admin_panel/settings/dev.py @@ -0,0 +1,27 @@ +# Set the environment variable DJANGO_SETTINGS_MODULE="admin_panel.settings.dev" +# to enable debug mode and developer tools + +from .local import * + +DEBUG = True + +# adds django debug toolbar +INSTALLED_APPS.append("debug_toolbar") +MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") +MIDDLEWARE.append("pyinstrument.middleware.ProfilerMiddleware") +DEBUG_TOOLBAR_PANELS = [ + "debug_toolbar.panels.history.HistoryPanel", + "debug_toolbar.panels.versions.VersionsPanel", + "debug_toolbar.panels.timer.TimerPanel", + "debug_toolbar.panels.settings.SettingsPanel", + "debug_toolbar.panels.headers.HeadersPanel", + "debug_toolbar.panels.request.RequestPanel", + "debug_toolbar.panels.sql.SQLPanel", + "debug_toolbar.panels.staticfiles.StaticFilesPanel", + "debug_toolbar.panels.templates.TemplatesPanel", + "debug_toolbar.panels.alerts.AlertsPanel", + # 'debug_toolbar.panels.cache.CachePanel', # this is making the page huge + "debug_toolbar.panels.signals.SignalsPanel", + "debug_toolbar.panels.redirects.RedirectsPanel", + # "debug_toolbar.panels.profiling.ProfilingPanel", +] diff --git a/admin_panel/admin_panel/settings/local.py b/admin_panel/admin_panel/settings/local.py new file mode 100644 index 00000000..7f6503ca --- /dev/null +++ b/admin_panel/admin_panel/settings/local.py @@ -0,0 +1,4 @@ +from .base import * + +DEBUG = True +SECRET_KEY = "insecure" diff --git a/admin_panel/admin_panel/settings/production.example.py b/admin_panel/admin_panel/settings/production.example.py new file mode 100644 index 00000000..429eb44d --- /dev/null +++ b/admin_panel/admin_panel/settings/production.example.py @@ -0,0 +1,17 @@ +# Copy this file as "production.py" and set the environment variable +# DJANGO_SETTINGS_MODULE="admin_panel.settings.production" to enable serving over the internet + +from .base import * + +DEBUG = False + +# Generate a long random string here for your secret key. It is important to keep it secret, +# leaking it could allow attackers to do privilege escalation. +# A good way to generate a long key is to run "pwgen 64 1" on Linux +SECRET_KEY = None + +ALLOWED_HOSTS = [ + "localhost", + # place the domain of your website here + # "ballsdex.com" +] diff --git a/admin_panel/admin_panel/urls.py b/admin_panel/admin_panel/urls.py new file mode 100644 index 00000000..77ce70a1 --- /dev/null +++ b/admin_panel/admin_panel/urls.py @@ -0,0 +1,28 @@ +from django.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import include, path + +admin_urls = admin.site.get_urls() +admin_urls[1].default_args["extra_context"] = { # type: ignore + "pwlogin": "django.contrib.auth.backends.ModelBackend" in settings.AUTHENTICATION_BACKENDS +} + +urlpatterns = ( + [ + path("/action-forms/", include("django_admin_action_forms.urls")), + path("", (admin_urls, "admin", admin.site.name)), + path("", include("preview.urls")), + path("", include("social_django.urls", namespace="social")), + ] + + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +) + +if "debug_toolbar" in settings.INSTALLED_APPS: + try: + from debug_toolbar.toolbar import debug_toolbar_urls + except ImportError: + pass + else: + urlpatterns.extend(debug_toolbar_urls()) diff --git a/admin_panel/admin_panel/webhook.py b/admin_panel/admin_panel/webhook.py new file mode 100644 index 00000000..160b540a --- /dev/null +++ b/admin_panel/admin_panel/webhook.py @@ -0,0 +1,37 @@ +import logging +from typing import Literal, overload + +import aiohttp +import discord +from discord.utils import MISSING + +from ballsdex.settings import settings + +log = logging.getLogger(__name__) + + +@overload +async def notify_admins(message: str = MISSING, *, wait: Literal[False]) -> None: ... # noqa: E704 +@overload # noqa: E302 +async def notify_admins( # noqa: E704 + message: str = MISSING, *, wait: Literal[True] = True +) -> discord.WebhookMessage: ... + + +async def notify_admins( + message: str = MISSING, *, wait: bool = True, **kwargs +) -> discord.WebhookMessage | None: + """ + Send a message to the configured Discord webhook. Additional arguments are described here: + https://discordpy.readthedocs.io/en/latest/api.html#discord.Webhook.send + + Set `wait` to `False` to ignore the resulting messsage, or failures to send it. + """ + if not settings.webhook_url: + log.warning(f"Discord webhook URL not configured, attempted to send: {message}") + return + async with aiohttp.ClientSession() as session: + webhook = discord.Webhook.from_url(settings.webhook_url, session=session) + return await webhook.send( + message, username="Ballsdex admin panel", wait=wait, **kwargs # type: ignore + ) diff --git a/admin_panel/admin_panel/wsgi.py b/admin_panel/admin_panel/wsgi.py new file mode 100644 index 00000000..a467ac8b --- /dev/null +++ b/admin_panel/admin_panel/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "admin_panel.settings") + +application = get_wsgi_application() diff --git a/admin_panel/bd_models/__init__.py b/admin_panel/bd_models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/admin_panel/bd_models/admin/__init__.py b/admin_panel/bd_models/admin/__init__.py new file mode 100644 index 00000000..b0aec561 --- /dev/null +++ b/admin_panel/bd_models/admin/__init__.py @@ -0,0 +1,17 @@ +from .ball import BallAdmin, EconomyAdmin, RegimeAdmin +from .ball_instance import BallInstanceAdmin +from .guild import GuildAdmin +from .player import PlayerAdmin +from .special import SpecialAdmin +from .trade import TradeAdmin + +__all__ = [ + "BallAdmin", + "EconomyAdmin", + "RegimeAdmin", + "BallInstanceAdmin", + "GuildAdmin", + "PlayerAdmin", + "SpecialAdmin", + "TradeAdmin", +] diff --git a/admin_panel/bd_models/admin/ball.py b/admin_panel/bd_models/admin/ball.py new file mode 100644 index 00000000..c91dc062 --- /dev/null +++ b/admin_panel/bd_models/admin/ball.py @@ -0,0 +1,195 @@ +from typing import TYPE_CHECKING, Any + +from django.contrib import admin +from django.contrib.admin.utils import quote +from django.forms import Textarea +from django.urls import reverse +from django.utils.html import format_html +from django.utils.safestring import mark_safe +from django.utils.text import capfirst + +from ..models import Ball, BallInstance, Economy, Regime, TradeObject, transform_media + +if TYPE_CHECKING: + from django.db.models import Field, Model + from django.http import HttpRequest + + +@admin.register(Regime) +class RegimeAdmin(admin.ModelAdmin): + list_display = ("name", "background_image", "pk") + search_fields = ("name",) + + @admin.display() + def background_image(self, obj: Regime): + return mark_safe( + f'' + ) + + def get_deleted_objects( + self, objs: "list[Regime]", request: "HttpRequest" + ) -> tuple[list[Any], dict[str, int], set[Any], list[Any]]: + regime_ids = [x.pk for x in objs] + model_count = { + "regimes": len(regime_ids), + "balls": Ball.objects.filter(regime_id__in=regime_ids).count(), + "ball instances": BallInstance.objects.filter(ball__regime_id__in=regime_ids).count(), + "trade objects": TradeObject.objects.filter( + ballinstance__ball__regime_id__in=regime_ids + ).count(), + } + + def format_callback(obj: "Model"): + opts = obj._meta + admin_url = reverse( + "%s:%s_%s_change" % (self.admin_site.name, opts.app_label, opts.model_name), + None, + (quote(obj.pk),), + ) + # Display a link to the admin page. + return format_html( + '{}: {}', capfirst(opts.verbose_name), admin_url, obj + ) + + text = [] + for regime in objs: + subtext = [] + for ball in Ball.objects.filter(regime=regime): + subtext.append(format_callback(ball)) + text.append(format_callback(regime)) + text.append(subtext) + + return ( + [ + "Displaying Ball related objects (instances and trade objects) " + "is too expensive and has been disabled.", + *text, + ], + model_count, + set(), + [], + ) + + +@admin.register(Economy) +class EconomyAdmin(admin.ModelAdmin): + list_display = ("name", "icon_image", "pk") + search_fields = ("name",) + + @admin.display() + def icon_image(self, obj: Economy): + return mark_safe(f'') + + +@admin.register(Ball) +class BallAdmin(admin.ModelAdmin): + autocomplete_fields = ("regime", "economy") + readonly_fields = ("collection_image", "spawn_image") + save_on_top = True + fieldsets = [ + ( + None, + { + "fields": [ + "country", + "health", + "attack", + "rarity", + "emoji_id", + "economy", + "regime", + ], + }, + ), + ( + "Assets", + { + "description": "You must have permission from the copyright holder " + "to use the files you're uploading!", + "fields": [ + "spawn_image", + "wild_card", + "collection_image", + "collection_card", + "credits", + ], + }, + ), + ( + "Ability", + { + "description": "The ability of the countryball", + "fields": ["capacity_name", "capacity_description"], + }, + ), + ( + "Advanced", + { + "description": "Advanced settings", + "classes": ["collapse"], + "fields": [ + "enabled", + "tradeable", + "short_name", + "catch_names", + "translations", + "capacity_logic", + ], + }, + ), + ] + + list_display = [ + "country", + "pk", + "emoji", + "rarity", + "capacity_name", + "health", + "attack", + "enabled", + ] + list_editable = ["enabled", "rarity"] + list_filter = ["enabled", "tradeable", "regime", "economy", "created_at"] + ordering = ["-created_at"] + + search_fields = [ + "country", + "capacity_name", + "capacity_description", + "catch_names", + "translations", + "credits", + "pk", + ] + search_help_text = ( + "Search for countryball name, ID, ability name/content, " + "credits, catch names or translations" + ) + + @admin.display(description="Emoji") + def emoji(self, obj: Ball): + return mark_safe( + f'' + ) + + def formfield_for_dbfield( + self, db_field: "Field[Any, Any]", request: "HttpRequest | None", **kwargs: Any + ) -> "Field[Any, Any] | None": + if db_field.name == "capacity_description": + kwargs["widget"] = Textarea() + return super().formfield_for_dbfield(db_field, request, **kwargs) # type: ignore + + def get_deleted_objects( + self, objs: "list[Ball]", request: "HttpRequest" + ) -> tuple[list[str], dict[str, int], set[Any], list[Any]]: + instances = BallInstance.objects.filter(ball_id__in=set(x.pk for x in objs)) + if len(instances) < 500: + return super().get_deleted_objects(objs, request) # type: ignore + model_count = { + "balls": len(objs), + "ball instances": len(instances), + "trade objects": TradeObject.objects.filter(ballinstance_id__in=instances).count(), + } + return ["Too long to display"], model_count, set(), [] diff --git a/admin_panel/bd_models/admin/ball_instance.py b/admin_panel/bd_models/admin/ball_instance.py new file mode 100644 index 00000000..32e06fb8 --- /dev/null +++ b/admin_panel/bd_models/admin/ball_instance.py @@ -0,0 +1,113 @@ +from typing import TYPE_CHECKING, Any + +from admin_auto_filters.filters import AutocompleteFilter +from django.contrib import admin, messages +from django.db.models import Prefetch + +from ..models import BallInstance, Player, Trade, TradeObject +from ..utils import ApproxCountPaginator + +if TYPE_CHECKING: + from django.db.models import QuerySet + from django.http import HttpRequest, HttpResponse + + +class SpecialFilter(AutocompleteFilter): + title = "special" + field_name = "special" + + +class BallFilter(AutocompleteFilter): + title = "countryball" + field_name = "ball" + + +class PlayerFilter(AutocompleteFilter): + title = "player" + field_name = "player" + + +@admin.register(BallInstance) +class BallInstanceAdmin(admin.ModelAdmin): + autocomplete_fields = ("player", "trade_player", "ball", "special") + save_on_top = True + fieldsets = [ + (None, {"fields": ("ball", "health_bonus", "attack_bonus", "special")}), + ("Ownership", {"fields": ("player", "favorite", "catch_date", "trade_player")}), + ( + "Advanced", + { + "classes": ("collapse",), + "fields": ("tradeable", "server_id", "spawned_time", "locked", "extra_data"), + }, + ), + ] + + list_display = ("description", "ball__country", "player", "health_bonus", "attack_bonus") + list_select_related = ("ball", "special", "player") + list_filter = (SpecialFilter, BallFilter, PlayerFilter, "tradeable", "favorite") + show_facets = admin.ShowFacets.NEVER # hide filtered counts (considerable slowdown) + show_full_result_count = False + paginator = ApproxCountPaginator + + search_help_text = "Search by hexadecimal ID or Discord ID" + search_fields = ("id",) # field is ignored, but required for the text area to show up + + def get_search_results( + self, request: "HttpRequest", queryset: "QuerySet[BallInstance]", search_term: str + ) -> "tuple[QuerySet[BallInstance], bool]": + if not search_term: + return super().get_search_results(request, queryset, search_term) # type: ignore + if search_term.isdigit() and 17 <= len(search_term) <= 22: + try: + player = Player.objects.get(discord_id=int(search_term)) + except Player.DoesNotExist: + return queryset.none(), False + return queryset.filter(player=player), False + try: + return queryset.filter(id=int(search_term, 16)), False + except ValueError: + messages.error(request, "Invalid search query") + return queryset.none(), False + + def change_view( + self, + request: "HttpRequest", + object_id: str, + form_url: str = "", + extra_context: dict[str, Any] | None = None, + ) -> "HttpResponse": + obj = BallInstance.objects.prefetch_related("player").get(id=object_id) + + def _get_trades(): + trade_ids = TradeObject.objects.filter(ballinstance=obj).values_list( + "trade_id", flat=True + ) + for trade in ( + Trade.objects.filter(id__in=trade_ids) + .order_by("-date") + .prefetch_related( + "player1", + "player2", + Prefetch( + "tradeobject_set", + queryset=TradeObject.objects.prefetch_related( + "ballinstance", "ballinstance__ball", "player" + ), + ), + ) + ): + player1_proposal = [ + x for x in trade.tradeobject_set.all() if x.player_id == trade.player1_id + ] + player2_proposal = [ + x for x in trade.tradeobject_set.all() if x.player_id == trade.player2_id + ] + yield { + "model": trade, + "proposals": (player1_proposal, player2_proposal), + } + + extra_context = extra_context or {} + extra_context["trades"] = list(_get_trades()) + return super().change_view(request, object_id, form_url, extra_context) diff --git a/admin_panel/bd_models/admin/guild.py b/admin_panel/bd_models/admin/guild.py new file mode 100644 index 00000000..393e9e9d --- /dev/null +++ b/admin_panel/bd_models/admin/guild.py @@ -0,0 +1,113 @@ +from typing import TYPE_CHECKING + +from asgiref.sync import async_to_sync +from django.contrib import admin, messages +from django.contrib.admin.utils import quote +from django.urls import reverse +from django.utils.html import format_html +from django_admin_action_forms import action_with_form +from django_admin_inline_paginator.admin import InlinePaginated +from nonrelated_inlines.admin import NonrelatedInlineMixin + +from admin_panel.webhook import notify_admins + +from ..forms import BlacklistActionForm, BlacklistedListFilter +from ..models import BallInstance, BlacklistedGuild, BlacklistHistory, GuildConfig +from ..utils import BlacklistTabular + +if TYPE_CHECKING: + from django.db.models import QuerySet + from django.http import HttpRequest + + +class BallInstanceGuildTabular(InlinePaginated, NonrelatedInlineMixin, admin.TabularInline): + model = BallInstance + fk_name = "player" + per_page = 50 + ordering = ("-catch_date",) + fields = ("description", "player", "catch_time", "catch_date") + readonly_fields = ("description", "player", "catch_time", "catch_date") + show_change_link = True + classes = ("collapse",) + can_delete = False + + def get_form_queryset(self, obj: GuildConfig): + return BallInstance.objects.filter(server_id=obj.guild_id).prefetch_related("player") + + @admin.display(description="Time to catch") + def catch_time(self, obj: BallInstance): + if obj.spawned_time: + return str(obj.spawned_time - obj.catch_date) + return "-" + + # adding a countryball cannot work from here since all fields are readonly + def has_add_permission(self, request: "HttpRequest", obj: GuildConfig) -> bool: + return False + + @admin.display(description="Player") + def player(self, obj: BallInstance): + opts = obj.player._meta + admin_url = reverse( + "%s:%s_%s_change" % (self.admin_site.name, opts.app_label, opts.model_name), + None, + (quote(obj.player.pk),), + ) + # Display a link to the admin page. + return format_html(f'{obj.player}') + + +@admin.register(GuildConfig) +class GuildAdmin(admin.ModelAdmin): + list_display = ("guild_id", "spawn_channel", "enabled", "silent", "blacklisted") + list_filter = ("enabled", "silent", BlacklistedListFilter) + show_facets = admin.ShowFacets.NEVER + + search_fields = ("guild_id", "spawn_channel") + search_help_text = "Search by guild ID or spawn channel ID" + + inlines = (BlacklistTabular, BallInstanceGuildTabular) + actions = ("blacklist_guilds",) + + @admin.display(description="Is blacklisted", boolean=True) + def blacklisted(self, obj: GuildConfig): + return BlacklistedGuild.objects.filter(discord_id=obj.guild_id).exists() + + @action_with_form( + BlacklistActionForm, description="Blacklist the selected guilds" + ) # type: ignore + def blacklist_guilds( + self, request: "HttpRequest", queryset: "QuerySet[GuildConfig]", data: dict + ): + reason = ( + data["reason"] + + f"\nDone through the admin panel by {request.user} ({request.user.pk})" + ) + blacklists: list[BlacklistedGuild] = [] + histories: list[BlacklistHistory] = [] + for guild in queryset: + if BlacklistedGuild.objects.filter(discord_id=guild.guild_id).exists(): + self.message_user( + request, f"Guild {guild.guild_id} is already blacklisted!", messages.ERROR + ) + return + blacklists.append( + BlacklistedGuild(discord_id=guild.guild_id, reason=reason, moderator_id=None) + ) + histories.append( + BlacklistHistory( + discord_id=guild.guild_id, reason=reason, moderator_id=0, id_type="guild" + ) + ) + BlacklistedGuild.objects.bulk_create(blacklists) + BlacklistHistory.objects.bulk_create(histories) + + self.message_user( + request, + f"Created blacklist for {queryset.count()} guild" + f"{"s" if queryset.count() > 1 else ""}. This will be applied after " + "reloading the bot's cache.", + ) + async_to_sync(notify_admins)( + f"{request.user} blacklisted guilds " + f'{", ".join([str(x.guild_id) for x in queryset])} for the reason: {data["reason"]}.' + ) diff --git a/admin_panel/bd_models/admin/player.py b/admin_panel/bd_models/admin/player.py new file mode 100644 index 00000000..9da42a0d --- /dev/null +++ b/admin_panel/bd_models/admin/player.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from asgiref.sync import async_to_sync +from django.contrib import admin, messages +from django.contrib.admin.utils import quote +from django.urls import reverse +from django.utils.html import format_html +from django_admin_action_forms import action_with_form +from django_admin_inline_paginator.admin import TabularInlinePaginated + +from admin_panel.webhook import notify_admins + +from ..forms import BlacklistActionForm, BlacklistedListFilter +from ..models import BallInstance, BlacklistedID, BlacklistHistory, GuildConfig, Player +from ..utils import BlacklistTabular + +if TYPE_CHECKING: + from django.db.models import QuerySet + from django.http import HttpRequest + + +class BallInstanceTabular(TabularInlinePaginated): + model = BallInstance + fk_name = "player" + per_page = 20 + ordering = ("-catch_date",) + fields = ("description", "server", "catch_time", "catch_date") + readonly_fields = ("description", "server", "catch_time", "catch_date") + show_change_link = True + classes = ("collapse",) + can_delete = False + + @admin.display(description="Time to catch") + def catch_time(self, obj: BallInstance): + if obj.spawned_time: + return str(obj.spawned_time - obj.catch_date) + return "-" + + # adding a countryball cannot work from here since all fields are readonly + def has_add_permission(self, request: "HttpRequest", obj: "Player") -> bool: + return False + + @admin.display(description="Server") + def server(self, obj: BallInstance): + guild = GuildConfig.objects.get(guild_id=obj.server_id) + opts = guild._meta + admin_url = reverse( + "%s:%s_%s_change" % (self.admin_site.name, opts.app_label, opts.model_name), + None, + (quote(guild.pk),), + ) + # Display a link to the admin page. + return format_html(f'{guild}') + + +@admin.register(Player) +class PlayerAdmin(admin.ModelAdmin): + save_on_top = True + inlines = (BlacklistTabular, BallInstanceTabular) + + list_display = ("discord_id", "pk", "blacklisted") + list_filter = (BlacklistedListFilter,) + show_facets = admin.ShowFacets.NEVER + + search_fields = ("discord_id",) + search_help_text = "Search for a Discord ID" + + actions = ("blacklist_users",) + + @admin.display(description="Is blacklisted", boolean=True) + def blacklisted(self, obj: Player): + return obj.is_blacklisted() + + @action_with_form( + BlacklistActionForm, description="Blacklist the selected users" + ) # type: ignore + def blacklist_users(self, request: "HttpRequest", queryset: "QuerySet[Player]", data: dict): + reason = ( + data["reason"] + + f"\nDone through the admin panel by {request.user} ({request.user.pk})" + ) + blacklists: list[BlacklistedID] = [] + histories: list[BlacklistHistory] = [] + for player in queryset: + if BlacklistedID.objects.filter(discord_id=player.discord_id).exists(): + self.message_user( + request, f"Player {player.discord_id} is already blacklisted!", messages.ERROR + ) + return + blacklists.append( + BlacklistedID(discord_id=player.discord_id, reason=reason, moderator_id=None) + ) + histories.append( + BlacklistHistory(discord_id=player.discord_id, reason=reason, moderator_id=0) + ) + BlacklistedID.objects.bulk_create(blacklists) + BlacklistHistory.objects.bulk_create(histories) + + self.message_user( + request, + f"Created blacklist for {queryset.count()} user{"s" if queryset.count() > 1 else ""}. " + "This will be applied after reloading the bot's cache.", + ) + async_to_sync(notify_admins)( + f"{request.user} blacklisted players " + f'{", ".join([str(x.discord_id) for x in queryset])} for the reason: {data["reason"]}.' + ) diff --git a/admin_panel/bd_models/admin/special.py b/admin_panel/bd_models/admin/special.py new file mode 100644 index 00000000..1fa9874a --- /dev/null +++ b/admin_panel/bd_models/admin/special.py @@ -0,0 +1,64 @@ +from typing import TYPE_CHECKING, Any + +from django.contrib import admin +from django.forms import Textarea +from django.utils.safestring import mark_safe + +from ..models import Special + +if TYPE_CHECKING: + from django.db.models import Field + from django.http import HttpRequest + + +@admin.register(Special) +class SpecialAdmin(admin.ModelAdmin): + save_on_top = True + fieldsets = [ + ( + None, + { + "fields": ["name", "catch_phrase", "rarity", "emoji", "background"], + }, + ), + ( + "Time range", + { + "fields": ["start_date", "end_date"], + "description": "An optional time range to make the event limited in time. As soon " + "as the event is loaded in the bot's cache, it will automatically load and unload " + "at the specified time.", + }, + ), + ( + "Advanced", + { + "fields": ["tradeable", "hidden"], + "classes": ["collapse"], + }, + ), + ] + + list_display = ["name", "pk", "emoji_display", "start_date", "end_date", "rarity", "hidden"] + list_editable = ["hidden", "rarity"] + list_filter = ["hidden", "tradeable"] + + search_fields = ["name", "catch_phrase", "pk"] + + @admin.display(description="Emoji") + def emoji_display(self, obj: Special): + return ( + mark_safe( + f'' + ) + if obj.emoji and obj.emoji.isdigit() + else obj.emoji + ) + + def formfield_for_dbfield( + self, db_field: "Field[Any, Any]", request: "HttpRequest | None", **kwargs: Any + ) -> "Field[Any, Any] | None": + if db_field.name == "catch_phrase": + kwargs["widget"] = Textarea() + return super().formfield_for_dbfield(db_field, request, **kwargs) # type: ignore diff --git a/admin_panel/bd_models/admin/trade.py b/admin_panel/bd_models/admin/trade.py new file mode 100644 index 00000000..f9631c63 --- /dev/null +++ b/admin_panel/bd_models/admin/trade.py @@ -0,0 +1,115 @@ +import itertools +import re +from typing import TYPE_CHECKING, Any + +from django.contrib import admin, messages +from django.db.models import Prefetch, Q + +from ..models import BallInstance, Player, Trade, TradeObject + +if TYPE_CHECKING: + from django.db.models import QuerySet + from django.http import HttpRequest, HttpResponse + +DUAL_ID_RE = re.compile(r"^[0-9]{17,20} [0-9]{17,20}$") + + +@admin.register(Trade) +class TradeAdmin(admin.ModelAdmin): + fields = ("player1", "player2", "date") + list_display = ("__str__", "player1", "player1_items", "player2", "player2_items") + readonly_fields = ("date",) + autocomplete_fields = ("player1", "player2") + + search_help_text = ( + "Search by hexadecimal ID of a trade, Discord ID of a player, " + "or two Discord IDs separated by a space for further filtering." + ) + search_fields = ("id",) # field is ignored, but required for the text area to show up + show_full_result_count = False + + def get_search_results( + self, request: "HttpRequest", queryset: "QuerySet[BallInstance]", search_term: str + ) -> "tuple[QuerySet[BallInstance], bool]": + if not search_term: + return super().get_search_results(request, queryset, search_term) # type: ignore + if search_term.isdigit() and 17 <= len(search_term) <= 20: + try: + player = Player.objects.get(discord_id=int(search_term)) + except Player.DoesNotExist: + messages.error(request, "Player does not exist in the database.") + return queryset.none(), False + return queryset.filter(Q(player1=player) | Q(player2=player)), False + elif DUAL_ID_RE.match(search_term.strip()): + id1, id2 = search_term.strip().split(" ") + try: + player1 = Player.objects.get(discord_id=int(id1)) + except Player.DoesNotExist: + messages.error(request, f"First player ({id1}) does not exist") + return queryset.none(), False + try: + player2 = Player.objects.get(discord_id=int(id2)) + except Player.DoesNotExist: + messages.error(request, f"Second player ({id2}) does not exist") + return queryset.none(), False + return ( + queryset.filter( + Q(player1=player1, player2=player2) | Q(player2=player1, player1=player2) + ), + False, + ) + try: + return queryset.filter(id=int(search_term, 16)), False + except ValueError: + messages.error(request, "Invalid search query") + return queryset.none(), False + + def get_queryset(self, request: "HttpRequest") -> "QuerySet[Trade]": + qs: "QuerySet[Trade]" = super().get_queryset(request) + return qs.prefetch_related( + "player1", + "player2", + Prefetch( + "tradeobject_set", queryset=TradeObject.objects.prefetch_related("ballinstance") + ), + ) + + # It is important to use .all() and manually filter in python rather than using .filter.count + # since the property is already prefetched and cached. Using .filter forces a new query + def player1_items(self, obj: Trade): + return len([None for x in obj.tradeobject_set.all() if x.player_id == obj.player1_id]) + + def player2_items(self, obj: Trade): + return len([None for x in obj.tradeobject_set.all() if x.player_id == obj.player2_id]) + + # The Trade model object is needed in `change_view`, but the admin does not provide it yet + # at this time. To avoid making the same query twice and slowing down the page loading, + # the model is cached here + def get_object(self, request: "HttpRequest", object_id: str, from_field: None = None) -> Trade: + if not hasattr(request, "object"): + request.object = self.get_queryset(request).get(id=object_id) # type: ignore + return request.object # type: ignore + + # This adds extra context to the template, needed for the display of TradeObject models + def change_view( + self, + request: "HttpRequest", + object_id: str, + form_url: str = "", + extra_context: dict[str, Any] | None = None, + ) -> "HttpResponse": + obj = self.get_object(request, object_id) + + # force queryset evaluation now to avoid double evaluation (with the len below) + objects = list(obj.tradeobject_set.all()) + player1_objects = [x for x in objects if x.player_id == obj.player1_id] + player2_objects = [x for x in objects if x.player_id == obj.player2_id] + objects = itertools.zip_longest(player1_objects, player2_objects) + + extra_context = extra_context or {} + extra_context["player1"] = obj.player1 + extra_context["player2"] = obj.player2 + extra_context["trade_objects"] = objects + extra_context["player1_len"] = len(player1_objects) + extra_context["player2_len"] = len(player2_objects) + return super().change_view(request, object_id, form_url, extra_context) diff --git a/admin_panel/bd_models/apps.py b/admin_panel/bd_models/apps.py new file mode 100644 index 00000000..aeba6d23 --- /dev/null +++ b/admin_panel/bd_models/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class BdModelsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "bd_models" + verbose_name = "Ballsdex models" diff --git a/admin_panel/bd_models/forms.py b/admin_panel/bd_models/forms.py new file mode 100644 index 00000000..b4d89492 --- /dev/null +++ b/admin_panel/bd_models/forms.py @@ -0,0 +1,41 @@ +from typing import TYPE_CHECKING, Any + +from django import forms +from django.contrib import admin +from django.db.models import Exists, OuterRef +from django_admin_action_forms import AdminActionForm + +from .models import BlacklistedGuild, BlacklistedID, Player + +if TYPE_CHECKING: + from django.db.models import QuerySet + from django.http import HttpRequest + + from .admin import GuildAdmin, PlayerAdmin + from .models import GuildConfig + + +class BlacklistActionForm(AdminActionForm): + reason = forms.CharField(label="Reason", required=True) + + +class BlacklistedListFilter(admin.SimpleListFilter): + title = "blacklisted" + parameter_name = "blacklisted" + + def lookups( + self, request: "HttpRequest", model_admin: "PlayerAdmin | GuildAdmin" + ) -> list[tuple[Any, str]]: + return [(True, "True"), (False, "False")] + + def queryset( + self, request: "HttpRequest", queryset: "QuerySet[Player | GuildConfig]" + ) -> "QuerySet[Player | GuildConfig]": + if self.value() is None: + return queryset + if queryset.model == Player: + annotation = Exists(BlacklistedID.objects.filter(discord_id=OuterRef("discord_id"))) + else: + annotation = Exists(BlacklistedGuild.objects.filter(discord_id=OuterRef("guild_id"))) + + return queryset.annotate(listed=annotation).filter(listed=self.value()) diff --git a/admin_panel/bd_models/migrations/0001_initial.py b/admin_panel/bd_models/migrations/0001_initial.py new file mode 100644 index 00000000..2c81d588 --- /dev/null +++ b/admin_panel/bd_models/migrations/0001_initial.py @@ -0,0 +1,451 @@ +# Generated by Django 5.1.4 on 2025-01-06 14:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Ball", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("country", models.CharField(max_length=48, unique=True)), + ("health", models.IntegerField(help_text="Ball health stat")), + ("attack", models.IntegerField(help_text="Ball attack stat")), + ("rarity", models.FloatField(help_text="Rarity of this ball")), + ("emoji_id", models.BigIntegerField(help_text="Emoji ID for this ball")), + ( + "wild_card", + models.ImageField( + help_text="Image used when a new ball spawns in the wild", + max_length=200, + upload_to="", + ), + ), + ( + "collection_card", + models.ImageField( + help_text="Image used when displaying balls", max_length=200, upload_to="" + ), + ), + ( + "credits", + models.CharField(help_text="Author of the collection artwork", max_length=64), + ), + ( + "capacity_name", + models.CharField( + help_text="Name of the countryball's capacity", max_length=64 + ), + ), + ( + "capacity_description", + models.CharField( + help_text="Description of the countryball's capacity", max_length=256 + ), + ), + ( + "capacity_logic", + models.JSONField(editable=False, help_text="Effect of this capacity"), + ), + ( + "enabled", + models.BooleanField(help_text="Enables spawning and show in completion"), + ), + ( + "short_name", + models.CharField( + blank=True, + help_text="An alternative shorter name used only when generating the " + "card, if the base name is too long.", + max_length=12, + null=True, + ), + ), + ( + "catch_names", + models.TextField( + blank=True, + help_text="Additional possible names for catching this ball, separated " + "by semicolons", + null=True, + ), + ), + ( + "tradeable", + models.BooleanField(help_text="Whether this ball can be traded with others"), + ), + ("created_at", models.DateTimeField(auto_now_add=True, null=True)), + ("translations", models.TextField(blank=True, null=True)), + ], + options={ + "db_table": "ball", + "managed": False, + }, + ), + migrations.CreateModel( + name="BallInstance", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("catch_date", models.DateTimeField()), + ("health_bonus", models.IntegerField()), + ("attack_bonus", models.IntegerField()), + ("favorite", models.BooleanField()), + ( + "server_id", + models.BigIntegerField( + blank=True, + help_text="Discord server ID where this ball was caught", + null=True, + ), + ), + ("tradeable", models.BooleanField()), + ("extra_data", models.JSONField()), + ( + "locked", + models.DateTimeField( + blank=True, + help_text="If the instance was locked for a trade and when", + null=True, + ), + ), + ("spawned_time", models.DateTimeField(blank=True, null=True)), + ], + options={ + "db_table": "ballinstance", + "managed": False, + }, + ), + migrations.CreateModel( + name="BlacklistedGuild", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("discord_id", models.BigIntegerField(help_text="Discord Guild ID", unique=True)), + ("reason", models.TextField(blank=True, null=True)), + ("date", models.DateTimeField(auto_now_add=True, null=True)), + ("moderator_id", models.BigIntegerField(blank=True, null=True)), + ], + options={ + "db_table": "blacklistedguild", + "managed": False, + }, + ), + migrations.CreateModel( + name="BlacklistedID", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("discord_id", models.BigIntegerField(help_text="Discord user ID", unique=True)), + ("reason", models.TextField(blank=True, null=True)), + ("date", models.DateTimeField(auto_now_add=True, null=True)), + ("moderator_id", models.BigIntegerField(blank=True, null=True)), + ], + options={ + "db_table": "blacklistedid", + "managed": False, + }, + ), + migrations.CreateModel( + name="BlacklistHistory", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("discord_id", models.BigIntegerField(help_text="Discord ID")), + ("moderator_id", models.BigIntegerField(help_text="Discord Moderator ID")), + ("reason", models.TextField(blank=True, null=True)), + ("date", models.DateTimeField(auto_now_add=True)), + ("id_type", models.CharField(default="user", max_length=64)), + ("action_type", models.CharField(default="blacklist", max_length=64)), + ], + options={ + "verbose_name_plural": "blacklisthistories", + "db_table": "blacklisthistory", + "managed": False, + }, + ), + migrations.CreateModel( + name="Block", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("date", models.DateTimeField(auto_now_add=True)), + ], + options={ + "db_table": "block", + "managed": False, + }, + ), + migrations.CreateModel( + name="Economy", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("name", models.CharField(max_length=64)), + ( + "icon", + models.ImageField(help_text="512x512 PNG image", max_length=200, upload_to=""), + ), + ], + options={ + "verbose_name_plural": "economies", + "db_table": "economy", + "managed": False, + }, + ), + migrations.CreateModel( + name="Friendship", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("since", models.DateTimeField(auto_now_add=True)), + ], + options={ + "db_table": "friendship", + "managed": False, + }, + ), + migrations.CreateModel( + name="Guildconfig", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("guild_id", models.BigIntegerField(help_text="Discord guild ID", unique=True)), + ( + "spawn_channel", + models.BigIntegerField( + blank=True, + help_text="Discord channel ID where balls will spawn", + null=True, + ), + ), + ( + "enabled", + models.BooleanField( + help_text="Whether the bot will spawn countryballs in this guild" + ), + ), + ("silent", models.BooleanField()), + ], + options={ + "db_table": "guildconfig", + "managed": False, + }, + ), + migrations.CreateModel( + name="Player", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("discord_id", models.BigIntegerField(help_text="Discord user ID", unique=True)), + ( + "donation_policy", + models.SmallIntegerField( + choices=[ + (1, "Always Accept"), + (2, "Request Approval"), + (3, "Always Deny"), + (4, "Friends Only"), + ], + help_text="How you want to handle donations", + ), + ), + ( + "privacy_policy", + models.SmallIntegerField( + choices=[(1, "Allow"), (2, "Deny"), (3, "Same Server"), (4, "Friends")], + help_text="How you want to handle inventory privacy", + ), + ), + ( + "mention_policy", + models.SmallIntegerField( + choices=[(1, "Allow"), (2, "Deny")], help_text="Control the bot's mentions" + ), + ), + ( + "friend_policy", + models.SmallIntegerField( + choices=[(1, "Allow"), (2, "Deny")], + help_text="Open or close your friend requests", + ), + ), + ], + options={ + "db_table": "player", + "managed": False, + }, + ), + migrations.CreateModel( + name="Regime", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("name", models.CharField(max_length=64)), + ( + "background", + models.ImageField( + help_text="1428x2000 PNG image", max_length=200, upload_to="" + ), + ), + ], + options={ + "db_table": "regime", + "managed": False, + }, + ), + migrations.CreateModel( + name="Special", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("name", models.CharField(max_length=64)), + ( + "catch_phrase", + models.CharField( + blank=True, + help_text="Sentence sent in bonus when someone catches a special card", + max_length=128, + null=True, + ), + ), + ( + "start_date", + models.DateTimeField( + blank=True, + help_text="Start time of the event. If blank, starts immediately", + null=True, + ), + ), + ( + "end_date", + models.DateTimeField( + blank=True, + help_text="End time of the event. If blank, the event is permanent", + null=True, + ), + ), + ( + "rarity", + models.FloatField( + help_text="Value between 0 and 1, chances of using this " + "special background." + ), + ), + ( + "emoji", + models.CharField( + blank=True, + help_text="Either a unicode character or a discord emoji ID", + max_length=20, + null=True, + ), + ), + ( + "background", + models.ImageField( + blank=True, + help_text="1428x2000 PNG image", + max_length=200, + null=True, + upload_to="", + ), + ), + ( + "tradeable", + models.BooleanField(help_text="Whether balls of this event can be traded"), + ), + ("hidden", models.BooleanField(help_text="Hides the event from user commands")), + ], + options={ + "db_table": "special", + "managed": False, + }, + ), + migrations.CreateModel( + name="Trade", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("date", models.DateTimeField(auto_now_add=True)), + ], + options={ + "db_table": "trade", + "managed": False, + }, + ), + migrations.CreateModel( + name="Tradeobject", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ], + options={ + "db_table": "tradeobject", + "managed": False, + }, + ), + ] diff --git a/admin_panel/bd_models/migrations/0002_move_upload_files.py b/admin_panel/bd_models/migrations/0002_move_upload_files.py new file mode 100644 index 00000000..9823dcd5 --- /dev/null +++ b/admin_panel/bd_models/migrations/0002_move_upload_files.py @@ -0,0 +1,108 @@ +# Generated by Django 5.1.4 on 2025-01-15 13:27 + +from pathlib import Path +from typing import TYPE_CHECKING + +from bd_models.models import Ball, Economy, Regime, Special +from django.db import migrations +from django.db.models import Case, ImageField, When +from django.db.models.expressions import F, Value +from django.db.models.functions import Concat, Replace + +if TYPE_CHECKING: + from django.apps.registry import Apps + from django.db.backends.base.schema import BaseDatabaseSchemaEditor + from django.db.models import Expression + +MEDIA = Path("./media") +OLD_STATIC = Path("../static/uploads") +CORE_SRC = Path("../ballsdex/core/image_generator/src") + +DEFAULT_ASSETS = [ + "capitalist.png", + "communist.png", + "democracy.png", + "dictatorship.png", + "shiny.png", + "union.png", +] + + +def _replace_text(column: str, reverse: bool = False) -> "dict[str, Expression]": + if reverse: + r = Case( + When( + **{f"{column}__in": DEFAULT_ASSETS}, + then=Concat( + Value("/ballsdex/core/image_generator/src/", output_field=ImageField()), + F(column), + ), + ), + default=Concat(Value("/static/uploads/", output_field=ImageField()), F(column)), + ) + else: + r = Replace( + Replace(F(column), Value("/ballsdex/core/image_generator/src/"), Value("")), + Value("/static/uploads/", output_field=ImageField()), + Value("", output_field=ImageField()), + ) + return {column: r} + + +def _check_reserved_names(): + for file in OLD_STATIC.glob("*"): + if file.name in DEFAULT_ASSETS: + raise ValueError( + f"The file {file.absolute()} has a reserved name and will conflict with Ballsdex " + "prodivded assets. You need to delete it or move it before proceeding. " + "Once this is done, reupload the asset via the new admin panel." + ) + + +def move_forwards(apps: "Apps", schema_editor: "BaseDatabaseSchemaEditor"): + if not OLD_STATIC.exists(follow_symlinks=False): + return + assert MEDIA.is_dir(follow_symlinks=False) + assert CORE_SRC.is_dir(follow_symlinks=False) + _check_reserved_names() + + Ball.objects.update(**_replace_text("wild_card"), **_replace_text("collection_card")) + Economy.objects.update(**_replace_text("icon")) + Regime.objects.update(**_replace_text("background")) + Special.objects.update(**_replace_text("background")) + + for file in OLD_STATIC.glob("*"): + if file.name == ".gitkeep": + continue + file.rename(MEDIA / file.name) + # the files in /ballsdex/core/image_generator/src/ will be moved by git + + +def move_backwards(apps: "Apps", schema_editor: "BaseDatabaseSchemaEditor"): + if not OLD_STATIC.exists(follow_symlinks=False): + return + assert MEDIA.is_dir(follow_symlinks=False) + + Ball.objects.update( + **_replace_text("wild_card", reverse=True), + **_replace_text("collection_card", reverse=True), + ) + Economy.objects.update(**_replace_text("icon", reverse=True)) + Regime.objects.update(**_replace_text("background", reverse=True)) + Special.objects.update(**_replace_text("background", reverse=True)) + + for file in MEDIA.glob("*"): + if file.name in DEFAULT_ASSETS: + continue + if file.name == ".gitkeep": + continue + file.rename(OLD_STATIC / file.name) + + +class Migration(migrations.Migration): + + dependencies = [ + ("bd_models", "0001_initial"), + ] + + operations = [migrations.RunPython(move_forwards, move_backwards, atomic=True)] diff --git a/admin_panel/bd_models/migrations/__init__.py b/admin_panel/bd_models/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/admin_panel/bd_models/models.py b/admin_panel/bd_models/models.py new file mode 100644 index 00000000..d215e382 --- /dev/null +++ b/admin_panel/bd_models/models.py @@ -0,0 +1,402 @@ +from __future__ import annotations + +from datetime import timedelta +from typing import Any, Iterable, cast + +from django.contrib import admin +from django.core.cache import cache +from django.db import models +from django.utils.safestring import SafeText, mark_safe +from django.utils.timezone import now + + +def transform_media(path: str) -> str: + return path.replace("/static/uploads/", "").replace( + "/ballsdex/core/image_generator/src/", "default/" + ) + + +def image_display(image_link: str) -> SafeText: + return mark_safe(f'') + + +class GuildConfig(models.Model): + guild_id = models.BigIntegerField(unique=True, help_text="Discord guild ID") + spawn_channel = models.BigIntegerField( + blank=True, null=True, help_text="Discord channel ID where balls will spawn" + ) + enabled = models.BooleanField( + help_text="Whether the bot will spawn countryballs in this guild" + ) + silent = models.BooleanField() + + def __str__(self) -> str: + return str(self.guild_id) + + class Meta: + managed = False + db_table = "guildconfig" + + +class DonationPolicy(models.IntegerChoices): + ALWAYS_ACCEPT = 1 + REQUEST_APPROVAL = 2 + ALWAYS_DENY = 3 + FRIENDS_ONLY = 4 + + +class PrivacyPolicy(models.IntegerChoices): + ALLOW = 1 + DENY = 2 + SAME_SERVER = 3 + FRIENDS = 4 + + +class MentionPolicy(models.IntegerChoices): + ALLOW = 1 + DENY = 2 + + +class FriendPolicy(models.IntegerChoices): + ALLOW = 1 + DENY = 2 + + +class Player(models.Model): + discord_id = models.BigIntegerField(unique=True, help_text="Discord user ID") + donation_policy = models.SmallIntegerField( + choices=DonationPolicy.choices, help_text="How you want to handle donations" + ) + privacy_policy = models.SmallIntegerField( + choices=PrivacyPolicy.choices, help_text="How you want to handle inventory privacy" + ) + mention_policy = models.SmallIntegerField( + choices=MentionPolicy.choices, help_text="Control the bot's mentions" + ) + friend_policy = models.SmallIntegerField( + choices=FriendPolicy.choices, help_text="Open or close your friend requests" + ) + + def is_blacklisted(self) -> bool: + blacklist = cast( + list[int], + cache.get_or_set( + "blacklist", + BlacklistedID.objects.all().values_list("discord_id", flat=True), + timeout=300, + ), + ) + return self.discord_id in blacklist + + def __str__(self) -> str: + return ( + f"{'\N{NO MOBILE PHONES} ' if self.is_blacklisted() else ''}#" + f"{self.pk} ({self.discord_id})" + ) + + class Meta: + managed = False + db_table = "player" + + +class Economy(models.Model): + name = models.CharField(max_length=64) + icon = models.ImageField(max_length=200, help_text="512x512 PNG image") + + def __str__(self) -> str: + return self.name + + class Meta: + managed = False + db_table = "economy" + verbose_name_plural = "economies" + + +class Regime(models.Model): + name = models.CharField(max_length=64) + background = models.ImageField(max_length=200, help_text="1428x2000 PNG image") + + def __str__(self) -> str: + return self.name + + class Meta: + managed = False + db_table = "regime" + + +class Special(models.Model): + name = models.CharField(max_length=64) + catch_phrase = models.CharField( + max_length=128, + blank=True, + null=True, + help_text="Sentence sent in bonus when someone catches a special card", + ) + start_date = models.DateTimeField( + blank=True, null=True, help_text="Start time of the event. If blank, starts immediately" + ) + end_date = models.DateTimeField( + blank=True, null=True, help_text="End time of the event. If blank, the event is permanent" + ) + rarity = models.FloatField( + help_text="Value between 0 and 1, chances of using this special background." + ) + emoji = models.CharField( + max_length=20, + blank=True, + null=True, + help_text="Either a unicode character or a discord emoji ID", + ) + background = models.ImageField( + max_length=200, blank=True, null=True, help_text="1428x2000 PNG image" + ) + tradeable = models.BooleanField( + help_text="Whether balls of this event can be traded", default=True + ) + hidden = models.BooleanField(help_text="Hides the event from user commands", default=False) + + def __str__(self) -> str: + return self.name + + class Meta: + managed = False + db_table = "special" + + +class Ball(models.Model): + country = models.CharField(unique=True, max_length=48) + health = models.IntegerField(help_text="Ball health stat") + attack = models.IntegerField(help_text="Ball attack stat") + rarity = models.FloatField(help_text="Rarity of this ball") + emoji_id = models.BigIntegerField(help_text="Emoji ID for this ball") + wild_card = models.ImageField( + max_length=200, + help_text="Image used when a new ball spawns in the wild", + ) + collection_card = models.ImageField( + max_length=200, help_text="Image used when displaying balls" + ) + credits = models.CharField(max_length=64, help_text="Author of the collection artwork") + capacity_name = models.CharField(max_length=64, help_text="Name of the countryball's capacity") + capacity_description = models.CharField( + max_length=256, help_text="Description of the countryball's capacity" + ) + capacity_logic = models.JSONField( + help_text="Effect of this capacity", blank=True, default=dict + ) + enabled = models.BooleanField( + help_text="Enables spawning and show in completion", default=True + ) + short_name = models.CharField( + max_length=12, + blank=True, + null=True, + help_text="An alternative shorter name used only when generating the card, " + "if the base name is too long.", + ) + catch_names = models.TextField( + blank=True, + null=True, + help_text="Additional possible names for catching this ball, separated by semicolons", + ) + tradeable = models.BooleanField( + help_text="Whether this ball can be traded with others", default=True + ) + economy = models.ForeignKey( + Economy, + on_delete=models.SET_NULL, + blank=True, + null=True, + help_text="Economical regime of this country", + ) + economy_id: int | None + regime = models.ForeignKey( + Regime, on_delete=models.CASCADE, help_text="Political regime of this country" + ) + regime_id: int + created_at = models.DateTimeField(blank=True, null=True, auto_now_add=True, editable=False) + translations = models.TextField(blank=True, null=True) + + def __str__(self) -> str: + return self.country + + @admin.display(description="Current collection card") + def collection_image(self) -> SafeText: + return image_display(str(self.collection_card)) + + @admin.display(description="Current spawn asset") + def spawn_image(self) -> SafeText: + return image_display(str(self.wild_card)) + + def save( + self, + force_insert: bool = False, + force_update: bool = False, + using: str | None = None, + update_fields: Iterable[str] | None = None, + ) -> None: + + def lower_catch_names(names: str | None) -> str | None: + if names: + return ";".join([x.strip() for x in names.split(";")]).lower() + + self.catch_names = lower_catch_names(self.catch_names) + self.translations = lower_catch_names(self.translations) + + return super().save(force_insert, force_update, using, update_fields) + + class Meta: + managed = False + db_table = "ball" + + +class BallInstance(models.Model): + catch_date = models.DateTimeField() + health_bonus = models.IntegerField() + attack_bonus = models.IntegerField() + ball = models.ForeignKey(Ball, on_delete=models.CASCADE) + ball_id: int + player = models.ForeignKey(Player, on_delete=models.CASCADE) + player_id: int + trade_player = models.ForeignKey( + Player, + on_delete=models.SET_NULL, + related_name="ballinstance_trade_player_set", + blank=True, + null=True, + ) + trade_player_id: int | None + favorite = models.BooleanField() + special = models.ForeignKey(Special, on_delete=models.SET_NULL, blank=True, null=True) + special_id: int | None + server_id = models.BigIntegerField( + blank=True, null=True, help_text="Discord server ID where this ball was caught" + ) + tradeable = models.BooleanField() + extra_data = models.JSONField(blank=True, default=dict) + locked = models.DateTimeField( + blank=True, null=True, help_text="If the instance was locked for a trade and when" + ) + spawned_time = models.DateTimeField(blank=True, null=True) + + def __getattribute__(self, name: str) -> Any: + if name == "ball": + balls = cast(list[Ball], cache.get_or_set("balls", Ball.objects.all(), timeout=30)) + for ball in balls: + if ball.pk == self.ball_id: + return ball + return super().__getattribute__(name) + + def __str__(self) -> str: + text = "" + if self.locked and self.locked > now() - timedelta(minutes=30): + text += "🔒" + if self.favorite: + text += "❤️" + if text: + text += " " + if self.special: + text += self.special.emoji or "" + return f"{text}#{self.pk:0X} {self.ball.country}" + + @admin.display(description="Countryball") + def description(self) -> SafeText: + text = str(self) + emoji = f'' + return mark_safe(f"{emoji} {text} ATK:{self.attack_bonus:+d}% HP:{self.health_bonus:+d}%") + + class Meta: + managed = False + db_table = "ballinstance" + unique_together = (("player", "id"),) + + +class BlacklistedID(models.Model): + discord_id = models.BigIntegerField(unique=True, help_text="Discord user ID") + reason = models.TextField(blank=True, null=True) + date = models.DateTimeField(blank=True, null=True, auto_now_add=True) + moderator_id = models.BigIntegerField(blank=True, null=True) + + class Meta: + managed = False + db_table = "blacklistedid" + + +class BlacklistedGuild(models.Model): + discord_id = models.BigIntegerField(unique=True, help_text="Discord Guild ID") + reason = models.TextField(blank=True, null=True) + date = models.DateTimeField(blank=True, null=True, auto_now_add=True) + moderator_id = models.BigIntegerField(blank=True, null=True) + + class Meta: + managed = False + db_table = "blacklistedguild" + + +class BlacklistHistory(models.Model): + discord_id = models.BigIntegerField(help_text="Discord ID") + moderator_id = models.BigIntegerField(help_text="Discord Moderator ID") + reason = models.TextField(blank=True, null=True) + date = models.DateTimeField(auto_now_add=True, editable=False) + id_type = models.CharField(max_length=64, default="user") + action_type = models.CharField(max_length=64, default="blacklist") + + class Meta: + managed = False + db_table = "blacklisthistory" + verbose_name_plural = "blacklisthistories" + + +class Trade(models.Model): + date = models.DateTimeField(auto_now_add=True, editable=False) + player1 = models.ForeignKey(Player, on_delete=models.CASCADE) + player1_id: int + player2 = models.ForeignKey(Player, on_delete=models.CASCADE, related_name="trade_player2_set") + player2_id: int + tradeobject_set: models.QuerySet[TradeObject] + + def __str__(self) -> str: + return f"Trade #{self.pk:0X}" + + class Meta: + managed = False + db_table = "trade" + + +class TradeObject(models.Model): + ballinstance = models.ForeignKey(BallInstance, on_delete=models.CASCADE) + ballinstance_id: int + player = models.ForeignKey(Player, on_delete=models.CASCADE) + player_id: int + trade = models.ForeignKey(Trade, on_delete=models.CASCADE) + trade_id: int + + class Meta: + managed = False + db_table = "tradeobject" + + +class Friendship(models.Model): + since = models.DateTimeField(auto_now_add=True, editable=False) + player1 = models.ForeignKey(Player, on_delete=models.CASCADE) + player1_id: int + player2 = models.ForeignKey( + Player, on_delete=models.CASCADE, related_name="friendship_player2_set" + ) + player2_id: int + + class Meta: + managed = False + db_table = "friendship" + + +class Block(models.Model): + date = models.DateTimeField(auto_now_add=True, editable=False) + player1 = models.ForeignKey(Player, on_delete=models.CASCADE) + player1_id: int + player2 = models.ForeignKey(Player, on_delete=models.CASCADE, related_name="block_player2_set") + player2_id: int + + class Meta: + managed = False + db_table = "block" diff --git a/admin_panel/bd_models/static/bd_models/style.css b/admin_panel/bd_models/static/bd_models/style.css new file mode 100644 index 00000000..177357cb --- /dev/null +++ b/admin_panel/bd_models/static/bd_models/style.css @@ -0,0 +1,25 @@ +#preview-sidebar { + flex-basis: 33%; + order: 1; + background: var(--darkened-bg); + border-left: none; + margin: 0 0 0 30px; + position: sticky; + top: 10px; +} + +#preview-sidebar h2 { + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 5px 15px; + margin-bottom: 12px; + border-bottom: none; +} + +#preview-content { + display: flex; + flex-direction: column; + margin-bottom: 15px; + margin: 10px; +} \ No newline at end of file diff --git a/admin_panel/bd_models/templates/generate_ball_preview.html b/admin_panel/bd_models/templates/generate_ball_preview.html new file mode 100644 index 00000000..3d219da3 --- /dev/null +++ b/admin_panel/bd_models/templates/generate_ball_preview.html @@ -0,0 +1,11 @@ +{% load static %} + + + + \ No newline at end of file diff --git a/admin_panel/bd_models/utils.py b/admin_panel/bd_models/utils.py new file mode 100644 index 00000000..d4b83bb5 --- /dev/null +++ b/admin_panel/bd_models/utils.py @@ -0,0 +1,51 @@ +from typing import TYPE_CHECKING + +from django.core.paginator import Paginator +from django.db import connection +from django.http import HttpRequest +from django.utils.functional import cached_property +from nonrelated_inlines.admin import NonrelatedTabularInline + +from .models import BlacklistHistory, Player + +if TYPE_CHECKING: + from .models import GuildConfig + + +class ApproxCountPaginator(Paginator): + @cached_property + def count(self): + + # if this object isn't empty, then it's a paginator that has been applied filters or search + if self.object_list.query.where.children: # type: ignore + return super().count + + with connection.cursor() as cursor: + cursor.execute( + "SELECT reltuples AS estimate FROM pg_class where relname = " + f"'{self.object_list.model._meta.db_table}';" # type: ignore + ) + result = int(cursor.fetchone()[0]) + if result < 100000: + return super().count + else: + return result + + +class BlacklistTabular(NonrelatedTabularInline): + model = BlacklistHistory + extra = 0 + can_delete = False + verbose_name_plural = "Blacklist history" + fields = ("date", "reason", "moderator_id", "action_type") + readonly_fields = ("date", "moderator_id", "action_type") + classes = ("collapse",) + + def has_add_permission(self, request: "HttpRequest", obj: "Player | GuildConfig") -> bool: + return False + + def get_form_queryset(self, obj: "Player | GuildConfig"): + if isinstance(obj, Player): + return BlacklistHistory.objects.filter(discord_id=obj.discord_id, id_type="user") + else: + return BlacklistHistory.objects.filter(discord_id=obj.guild_id, id_type="guild") diff --git a/admin_panel/manage.py b/admin_panel/manage.py new file mode 100644 index 00000000..abdcf46f --- /dev/null +++ b/admin_panel/manage.py @@ -0,0 +1,22 @@ +#!/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", "admin_panel.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() diff --git a/admin_panel/media/.gitkeep b/admin_panel/media/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/ballsdex/core/image_generator/src/capitalist.png b/admin_panel/media/capitalist.png old mode 100755 new mode 100644 similarity index 100% rename from ballsdex/core/image_generator/src/capitalist.png rename to admin_panel/media/capitalist.png diff --git a/ballsdex/core/image_generator/src/communist.png b/admin_panel/media/communist.png old mode 100755 new mode 100644 similarity index 100% rename from ballsdex/core/image_generator/src/communist.png rename to admin_panel/media/communist.png diff --git a/ballsdex/core/image_generator/src/democracy.png b/admin_panel/media/democracy.png old mode 100755 new mode 100644 similarity index 100% rename from ballsdex/core/image_generator/src/democracy.png rename to admin_panel/media/democracy.png diff --git a/ballsdex/core/image_generator/src/dictatorship.png b/admin_panel/media/dictatorship.png old mode 100755 new mode 100644 similarity index 100% rename from ballsdex/core/image_generator/src/dictatorship.png rename to admin_panel/media/dictatorship.png diff --git a/ballsdex/core/image_generator/src/shiny.png b/admin_panel/media/shiny.png similarity index 100% rename from ballsdex/core/image_generator/src/shiny.png rename to admin_panel/media/shiny.png diff --git a/ballsdex/core/image_generator/src/union.png b/admin_panel/media/union.png old mode 100755 new mode 100644 similarity index 100% rename from ballsdex/core/image_generator/src/union.png rename to admin_panel/media/union.png diff --git a/admin_panel/preview/__init__.py b/admin_panel/preview/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/admin_panel/preview/apps.py b/admin_panel/preview/apps.py new file mode 100644 index 00000000..cfa2486b --- /dev/null +++ b/admin_panel/preview/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PreviewConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "preview" diff --git a/admin_panel/preview/migrations/__init__.py b/admin_panel/preview/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/admin_panel/preview/urls.py b/admin_panel/preview/urls.py new file mode 100644 index 00000000..c2ab2f6c --- /dev/null +++ b/admin_panel/preview/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from .views import render_ballinstance, render_special + +urlpatterns = [ + path("ball/generate/", render_ballinstance), + path("special/generate/", render_special), +] diff --git a/admin_panel/preview/views.py b/admin_panel/preview/views.py new file mode 100644 index 00000000..077d078f --- /dev/null +++ b/admin_panel/preview/views.py @@ -0,0 +1,78 @@ +import os + +from django.contrib import messages +from django.http import HttpRequest, HttpResponse +from tortoise import Tortoise + +from ballsdex.__main__ import init_tortoise +from ballsdex.core.image_generator.image_gen import draw_card +from ballsdex.core.models import ( + Ball, + BallInstance, + Economy, + Regime, + Special, + balls, + economies, + regimes, + specials, +) + + +async def _refresh_cache(): + """ + Similar to the bot's `load_cache` function without the fancy display. Also handles + initializing the connection to Tortoise. + + This must be called on every request, since the image generation relies on cache and we + do *not* want caching in the admin panel to happen (since we're actively editing stuff). + """ + if not Tortoise._inited: + await init_tortoise(os.environ["BALLSDEXBOT_DB_URL"], skip_migrations=True) + balls.clear() + for ball in await Ball.all(): + balls[ball.pk] = ball + + regimes.clear() + for regime in await Regime.all(): + regimes[regime.pk] = regime + + economies.clear() + for economy in await Economy.all(): + economies[economy.pk] = economy + + specials.clear() + for special in await Special.all(): + specials[special.pk] = special + + +async def render_ballinstance(request: HttpRequest, ball_pk: int) -> HttpResponse: + await _refresh_cache() + + ball = await Ball.get(pk=ball_pk) + instance = BallInstance(ball=ball) + image = draw_card(instance, media_path="./media/") + + response = HttpResponse(content_type="image/png") + image.save(response, "PNG") # type: ignore + return response + + +async def render_special(request: HttpRequest, special_pk: int) -> HttpResponse: + await _refresh_cache() + + ball = await Ball.first() + if ball is None: + messages.warning( + request, + "You must create a countryball before being able to generate a special's preview.", + ) + return HttpResponse(status_code=422) + + special = await Special.get(pk=special_pk) + instance = BallInstance(ball=ball, special=special) + image = draw_card(instance, media_path="./media/") + + response = HttpResponse(content_type="image/png") + image.save(response, "PNG") # type: ignore + return response diff --git a/admin_panel/staticfiles/admin/css/override.css b/admin_panel/staticfiles/admin/css/override.css new file mode 100644 index 00000000..1bdf7d48 --- /dev/null +++ b/admin_panel/staticfiles/admin/css/override.css @@ -0,0 +1,30 @@ +.discord-row { +} + +.discord-row a.discord-button { + background: #5865F2; + display: flex; + justify-content: center; + width: fit-content; + margin: auto; + padding: 0; +} + +.discord-row a.discord-button:hover { + background: color-mix(in srgb, #5865F2, black 30%) +} + +.discord-button * { + padding: 10px; +} + +.discord-icon { + width: 1.2em; + background: color-mix(in srgb, transparent, black 20%); +} + +.button[disabled], input[type=submit][disabled], input[type=button][disabled], .discord-row a.disabled { + opacity: 0.4; + pointer-events: none; + cursor: default; +} diff --git a/admin_panel/staticfiles/admin/img/discord.svg b/admin_panel/staticfiles/admin/img/discord.svg new file mode 100644 index 00000000..7f9a31f0 --- /dev/null +++ b/admin_panel/staticfiles/admin/img/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin_panel/templates/admin/bd_models/ball/change_form.html b/admin_panel/templates/admin/bd_models/ball/change_form.html new file mode 100644 index 00000000..822b2a64 --- /dev/null +++ b/admin_panel/templates/admin/bd_models/ball/change_form.html @@ -0,0 +1,73 @@ + + +{% extends "admin/change_form.html" %} +{% load i18n admin_urls static admin_modify %} + +{% block content %} +{% if request.resolver_match.url_name == "bd_models_ball_change" %} + +
+ {% block object-tools %} + {% if change and not is_popup %} +
    + {% block object-tools-items %} + {% change_form_object_tools %} + {% endblock %} +
+ {% endif %} + {% endblock %} +
+
{% csrf_token %}{% block form_top %}{% endblock %} +
+ {% if is_popup %}{% endif %} + {% if to_field %}{% endif %} + {% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %} + {% if errors %} +

+ {% blocktranslate count counter=errors|length %}Please correct the error below.{% plural %}Please + correct the errors below.{% endblocktranslate %} +

+ {{ adminform.form.non_field_errors }} + {% endif %} + + {% block field_sets %} + {% for fieldset in adminform %} + {% include "admin/includes/fieldset.html" with heading_level=2 prefix="fieldset" id_prefix=0 id_suffix=forloop.counter0 %} + {% endfor %} + {% endblock %} + + {% block after_field_sets %}{% endblock %} + + {% block inline_field_sets %} + {% for inline_admin_formset in inline_admin_formsets %} + {% include inline_admin_formset.opts.template %} + {% endfor %} + {% endblock %} + + {% block after_related_objects %}{% endblock %} + + {% block submit_buttons_bottom %}{% submit_row %}{% endblock %} + + {% block admin_change_form_document_ready %} + + {% endblock %} + + {# JavaScript for prepopulated fields #} + {% prepopulated_fields_js %} + +
+
+ {% include "generate_ball_preview.html" %} +
+
+{% else %} + {{ block.super }} +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/admin_panel/templates/admin/bd_models/ballinstance/change_form.html b/admin_panel/templates/admin/bd_models/ballinstance/change_form.html new file mode 100644 index 00000000..4a42adb9 --- /dev/null +++ b/admin_panel/templates/admin/bd_models/ballinstance/change_form.html @@ -0,0 +1,45 @@ +{% extends "admin/change_form.html" %} + +{% block after_related_objects %} + + +{% endblock %} \ No newline at end of file diff --git a/admin_panel/templates/admin/bd_models/special/change_form.html b/admin_panel/templates/admin/bd_models/special/change_form.html new file mode 100644 index 00000000..dfc03162 --- /dev/null +++ b/admin_panel/templates/admin/bd_models/special/change_form.html @@ -0,0 +1,73 @@ + + +{% extends "admin/change_form.html" %} +{% load i18n admin_urls static admin_modify %} + +{% block content %} +{% if request.resolver_match.url_name == "bd_models_special_change" %} + +
+ {% block object-tools %} + {% if change and not is_popup %} +
    + {% block object-tools-items %} + {% change_form_object_tools %} + {% endblock %} +
+ {% endif %} + {% endblock %} +
+
{% csrf_token %}{% block form_top %}{% endblock %} +
+ {% if is_popup %}{% endif %} + {% if to_field %}{% endif %} + {% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %} + {% if errors %} +

+ {% blocktranslate count counter=errors|length %}Please correct the error below.{% plural %}Please + correct the errors below.{% endblocktranslate %} +

+ {{ adminform.form.non_field_errors }} + {% endif %} + + {% block field_sets %} + {% for fieldset in adminform %} + {% include "admin/includes/fieldset.html" with heading_level=2 prefix="fieldset" id_prefix=0 id_suffix=forloop.counter0 %} + {% endfor %} + {% endblock %} + + {% block after_field_sets %}{% endblock %} + + {% block inline_field_sets %} + {% for inline_admin_formset in inline_admin_formsets %} + {% include inline_admin_formset.opts.template %} + {% endfor %} + {% endblock %} + + {% block after_related_objects %}{% endblock %} + + {% block submit_buttons_bottom %}{% submit_row %}{% endblock %} + + {% block admin_change_form_document_ready %} + + {% endblock %} + + {# JavaScript for prepopulated fields #} + {% prepopulated_fields_js %} + +
+
+ {% include "generate_ball_preview.html" %} +
+
+{% else %} + {{ block.super }} +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/admin_panel/templates/admin/bd_models/trade/change_form.html b/admin_panel/templates/admin/bd_models/trade/change_form.html new file mode 100644 index 00000000..17823511 --- /dev/null +++ b/admin_panel/templates/admin/bd_models/trade/change_form.html @@ -0,0 +1,42 @@ +{% extends "admin/change_form.html" %} + +{% block after_related_objects %} + + +{% endblock %} \ No newline at end of file diff --git a/admin_panel/templates/admin/login.html b/admin_panel/templates/admin/login.html new file mode 100644 index 00000000..8b787cfd --- /dev/null +++ b/admin_panel/templates/admin/login.html @@ -0,0 +1,63 @@ +{% extends "admin/login.html" %} +{% load i18n static %} + +{% block extrastyle %}{{ block.super }} + +{% endblock %} + +{% block content %} +{% if form.errors and not form.non_field_errors %} +

+{% blocktranslate count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktranslate %} +

+{% endif %} + +{% if form.non_field_errors %} +{% for error in form.non_field_errors %} +

+ {{ error }} +

+{% endfor %} +{% endif %} + +
+ +{% if user.is_authenticated %} +

+{% blocktranslate trimmed %} + You are authenticated as {{ username }}, but are not authorized to + access this page. Would you like to login to a different account? +{% endblocktranslate %} +

+{% endif %} + +
{% csrf_token %} +
+ {{ form.username.errors }} + {{ form.username.label_tag }} {{ form.username }} +
+
+ {{ form.password.errors }} + {{ form.password.label_tag }} {{ form.password }} + +
+ {% url 'admin_password_reset' as password_reset_url %} + {% if password_reset_url %} + + {% endif %} +
+ +
+ +
+ + + +
+{% endblock %} \ No newline at end of file diff --git a/ballsdex/__main__.py b/ballsdex/__main__.py index 3e182616..e2c3bbc3 100755 --- a/ballsdex/__main__.py +++ b/ballsdex/__main__.py @@ -237,10 +237,13 @@ def filter(self, record): return True -async def init_tortoise(db_url: str): +async def init_tortoise(db_url: str, *, skip_migrations: bool = False): log.debug(f"Database URL: {db_url}") await Tortoise.init(config=TORTOISE_ORM) + if skip_migrations: + return + # migrations command = Command(TORTOISE_ORM, app="models") await command.init() diff --git a/ballsdex/core/admin/__init__.py b/ballsdex/core/admin/__init__.py deleted file mode 100644 index 9cae801d..00000000 --- a/ballsdex/core/admin/__init__.py +++ /dev/null @@ -1,91 +0,0 @@ -import os -import pathlib - -from fastapi import FastAPI -from fastapi_admin.app import app as admin_app -from fastapi_admin.exceptions import ( - forbidden_error_exception, - not_found_error_exception, - server_error_exception, - unauthorized_error_exception, -) -from fastapi_admin.providers.login import UsernamePasswordProvider -from redis import asyncio as aioredis -from starlette.middleware.cors import CORSMiddleware -from starlette.responses import RedirectResponse -from starlette.staticfiles import StaticFiles -from starlette.status import ( - HTTP_401_UNAUTHORIZED, - HTTP_403_FORBIDDEN, - HTTP_404_NOT_FOUND, - HTTP_500_INTERNAL_SERVER_ERROR, -) -from tortoise.contrib.fastapi import register_tortoise - -from ballsdex.__main__ import TORTOISE_ORM -from ballsdex.core.admin import resources, routes # noqa: F401 -from ballsdex.core.admin.resources import User - -BASE_DIR = pathlib.Path(".") - - -def init_fastapi_app() -> FastAPI: - app = FastAPI() - app.mount( - "/static", - StaticFiles(directory=BASE_DIR / "static"), - name="static", - ) - app.mount( - "/ballsdex/core/image_generator/src", - StaticFiles(directory=BASE_DIR / "ballsdex/core/image_generator/src"), - name="image_gen", - ) - - @app.get("/") - async def index(): - return RedirectResponse(url="/admin") - - admin_app.add_exception_handler( - HTTP_500_INTERNAL_SERVER_ERROR, server_error_exception # type: ignore - ) - admin_app.add_exception_handler(HTTP_404_NOT_FOUND, not_found_error_exception) # type: ignore - admin_app.add_exception_handler(HTTP_403_FORBIDDEN, forbidden_error_exception) # type: ignore - admin_app.add_exception_handler( - HTTP_401_UNAUTHORIZED, unauthorized_error_exception # type: ignore - ) - - @app.on_event("startup") - async def startup(): - redis = aioredis.from_url( - os.environ["BALLSDEXBOT_REDIS_URL"], decode_responses=True, encoding="utf8" - ) - await admin_app.configure( - logo_url="https://i.imgur.com/HwNKi5a.png", - template_folders=[os.path.join(BASE_DIR, "ballsdex", "templates")], - favicon_url="https://raw.githubusercontent.com/fastapi-admin/" # type: ignore - "fastapi-admin/dev/images/favicon.png", - providers=[ - UsernamePasswordProvider( - login_logo_url="https://preview.tabler.io/static/logo.svg", - admin_model=User, - ) - ], - redis=redis, - ) - - app.mount("/admin", admin_app) - app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - expose_headers=["*"], - ) - register_tortoise(app, config=TORTOISE_ORM) - - return app - - -_app = init_fastapi_app() diff --git a/ballsdex/core/admin/resources.py b/ballsdex/core/admin/resources.py deleted file mode 100755 index 87dc3442..00000000 --- a/ballsdex/core/admin/resources.py +++ /dev/null @@ -1,389 +0,0 @@ -import os -from typing import Any, List - -from fastapi_admin.app import app -from fastapi_admin.enums import Method -from fastapi_admin.file_upload import FileUpload -from fastapi_admin.resources import Action, Field, Link, Model -from fastapi_admin.widgets import displays, filters, inputs -from starlette.requests import Request - -from ballsdex.core.models import ( - Ball, - BallInstance, - BlacklistedGuild, - BlacklistedID, - Economy, - GuildConfig, - Player, - Regime, - Special, - User, -) - - -@app.register -class Home(Link): - label = "Home" - icon = "fas fa-home" - url = "/admin" - - -upload = FileUpload(uploads_dir=os.path.join(".", "static", "uploads")) - - -@app.register -class AdminResource(Model): - label = "Admin" - model = User - icon = "fas fa-user" - page_pre_title = "admin list" - page_title = "Admins" - filters = [ - filters.Search( - name="username", - label="Name", - search_mode="contains", - placeholder="Search for username", - ), - ] - fields = [ - "id", - "username", - Field( - name="password", - label="Password", - display=displays.InputOnly(), - input_=inputs.Password(), - ), - Field( - name="avatar", - label="Avatar", - display=displays.Image(width="40"), - input_=inputs.Image(null=True, upload=upload), - ), - "created_at", - ] - - async def cell_attributes(self, request: Request, obj: dict, field: Field) -> dict: - if field.name == "id": - return {"class": "bg-danger text-white"} - return await super().cell_attributes(request, obj, field) - - -@app.register -class SpecialResource(Model): - label = "Special events" - model = Special - icon = "fas fa-star" - page_pre_title = "special list" - page_title = "Special events list" - filters = [ - filters.Search( - name="name", label="Name", search_mode="icontains", placeholder="Search for events" - ), - filters.Boolean(name="hidden", label="Hidden"), - ] - fields = [ - "name", - "catch_phrase", - Field( - name="start_date", - label="Start date of the event", - display=displays.DateDisplay(), - input_=inputs.Date(help_text="Date when special balls will start spawning"), - ), - Field( - name="end_date", - label="End date of the event", - display=displays.DateDisplay(), - input_=inputs.Date(help_text="Date when special balls will stop spawning"), - ), - "rarity", - Field( - name="background", - label="Special background (1428x2000)", - display=displays.Image(width="40"), - input_=inputs.Image(upload=upload, null=True), - ), - "emoji", - "tradeable", - "hidden", - "credits", - ] - - async def get_actions(self, request: Request) -> List[Action]: - actions = await super().get_actions(request) - actions.append( - Action( - icon="fas fa-upload", - label="Generate card", - name="generate", - method=Method.GET, - ajax=False, - ) - ) - return actions - - -@app.register -class RegimeResource(Model): - label = "Regime" - model = Regime - icon = "fas fa-flag" - page_pre_title = "regime list" - page_title = "Regimes" - fields = [ - "name", - Field( - name="background", - label="Background (1428x2000)", - display=displays.Image(width="40"), - input_=inputs.Image(upload=upload, null=True), - ), - ] - - -@app.register -class EconomyResource(Model): - label = "Economy" - model = Economy - icon = "fas fa-coins" - page_pre_title = "economy list" - page_title = "Economies" - fields = [ - "name", - Field( - name="icon", - label="Icon (512x512)", - display=displays.Image(width="40"), - input_=inputs.Image(upload=upload, null=True), - ), - ] - - -class Emoji(displays.Display): - async def render(self, request: Request, value: Any): - return ( - f'' - ) - - -@app.register -class BallResource(Model): - label = "Ball" - model = Ball - page_size = 50 - icon = "fas fa-globe" - page_pre_title = "ball list" - page_title = "Balls" - filters = [ - filters.Search( - name="country", - label="Country", - search_mode="icontains", - placeholder="Search for balls", - ), - filters.ForeignKey(model=Regime, name="regime", label="Regime"), - filters.ForeignKey(model=Economy, name="economy", label="Economy"), - filters.Boolean(name="enabled", label="Enabled"), - filters.Boolean(name="tradeable", label="Tradeable"), - ] - fields = [ - "country", - "short_name", - "catch_names", - "translations", - "created_at", - "regime", - "economy", - "health", - "attack", - "rarity", - "enabled", - "tradeable", - Field( - name="emoji_id", - label="Emoji", - display=Emoji(), - input_=inputs.Input( - help_text="Emoji ID of this ball. Application emojis not supported. " - "Send \\:emoji-name: on Discord to obtain the ID." - ), - ), - Field( - name="wild_card", - label="Wild card", - display=displays.Image(width="40"), - input_=inputs.Image( - upload=upload, - null=True, - help_text="The file uploaded when this ball spawns. You certify that you have " - "the permission from the copyright holder to use this file.", - ), - ), - Field( - name="collection_card", - label="Collection card (16:9 ratio)", - display=displays.Image(width="40"), - input_=inputs.Image( - upload=upload, - null=True, - help_text="The image used to generate the collection card. You certify that " - "you have the permission from the copyright holder to use this file.", - ), - ), - "credits", - "capacity_name", - "capacity_description", - ] - - async def get_actions(self, request: Request) -> List[Action]: - actions = await super().get_actions(request) - actions.append( - Action( - icon="fas fa-upload", - label="Generate card", - name="generate", - method=Method.GET, - ajax=False, - ) - ) - return actions - - -@app.register -class BallInstanceResource(Model): - label = "Ball instance" - model = BallInstance - icon = "fas fa-atlas" - page_pre_title = "ball instances list" - page_title = "Ball instances" - filters = [ - filters.Search( - name="id", - label="Ball Instance ID", - placeholder="Search for ball IDs", - ), - filters.ForeignKey(model=Ball, name="ball", label="Ball"), - filters.ForeignKey(model=Special, name="special", label="Special"), - filters.Boolean(name="favorite", label="Favorite"), - filters.Search( - name="player__discord_id", - label="User ID", - placeholder="Search for Discord user ID", - ), - filters.Search( - name="server_id", - label="Server ID", - placeholder="Search for Discord server ID", - ), - filters.Boolean(name="tradeable", label="Tradeable"), - ] - fields = [ - "id", - "ball", - "player", - "catch_date", - "server_id", - "special", - "favorite", - "health_bonus", - "attack_bonus", - "tradeable", - ] - - -@app.register -class PlayerResource(Model): - label = "Player" - model = Player - icon = "fas fa-user" - page_pre_title = "player list" - page_title = "Players" - filters = [ - filters.Search( - name="discord_id", - label="ID", - search_mode="icontains", - placeholder="Filter by ID", - ), - ] - fields = [ - "discord_id", - "balls", - "donation_policy", - "privacy_policy", - ] - - -@app.register -class GuildConfigResource(Model): - label = "Guild config" - model = GuildConfig - icon = "fas fa-cog" - page_title = "Guild configs" - filters = [ - filters.Search( - name="guild_id", - label="ID", - search_mode="icontains", - placeholder="Filter by ID", - ), - ] - fields = ["guild_id", "spawn_channel", "enabled", "silent"] - - -@app.register -class BlacklistedIDResource(Model): - label = "Blacklisted user ID" - model = BlacklistedID - icon = "fas fa-user-lock" - page_title = "Blacklisted user IDs" - filters = [ - filters.Search( - name="discord_id", - label="ID", - search_mode="icontains", - placeholder="Filter by ID", - ), - filters.Search( - name="reason", - label="Reason", - search_mode="search", - placeholder="Search by reason", - ), - ] - fields = [ - "discord_id", - "reason", - "date", - ] - - -@app.register -class BlacklistedGuildIDResource(Model): - label = "Blacklisted Guild ID" - model = BlacklistedGuild - icon = "fas fa-lock" - page_title = "Blacklisted Guild IDs" - filters = [ - filters.Search( - name="guild_id", - label="ID", - search_mode="icontains", - placeholder="Filter by Guild ID", - ), - filters.Search( - name="reason", - label="Reason", - search_mode="search", - placeholder="Search by reason", - ), - ] - fields = [ - "discord_id", - "reason", - "date", - ] diff --git a/ballsdex/core/admin/routes.py b/ballsdex/core/admin/routes.py deleted file mode 100755 index 4b7aa572..00000000 --- a/ballsdex/core/admin/routes.py +++ /dev/null @@ -1,59 +0,0 @@ -from fastapi import Depends, Path -from fastapi_admin.app import app -from fastapi_admin.depends import get_current_admin, get_resources -from fastapi_admin.template import templates -from starlette.requests import Request -from starlette.responses import RedirectResponse, Response -from tortoise.exceptions import DoesNotExist - -from ballsdex.core.models import Ball, BallInstance, GuildConfig, Player, Special - - -@app.get("/") -async def home( - request: Request, - resources=Depends(get_resources), -): - if not request.state.admin: - return RedirectResponse(app.admin_path + "/login") - return templates.TemplateResponse( - "dashboard.html", - context={ - "request": request, - "resources": resources, - "ball_count": await Ball.all().count(), - "player_count": await Player.all().count(), - "guild_count": await GuildConfig.all().count(), - "resource_label": "Dashboard", - "page_pre_title": "overview", - "page_title": "Dashboard", - }, - ) - - -@app.get("/ball/generate/{pk}", dependencies=[Depends(get_current_admin)]) -async def generate_card( - request: Request, - pk: str = Path(...), -): - ball = await Ball.get(pk=pk).prefetch_related("regime", "economy") - temp_instance = BallInstance(ball=ball, player=await Player.first(), count=1) - buffer = temp_instance.draw_card() - return Response(content=buffer.read(), media_type="image/png") - - -@app.get("/special/generate/{pk}", dependencies=[Depends(get_current_admin)]) -async def generate_special_card( - request: Request, - pk: str = Path(...), -): - special = await Special.get(pk=pk) - try: - ball = await Ball.first().prefetch_related("regime", "economy") - except DoesNotExist: - return Response( - content="At least one ball must exist", status_code=422, media_type="text/html" - ) - temp_instance = BallInstance(ball=ball, special=special, player=await Player.first(), count=1) - buffer = temp_instance.draw_card() - return Response(content=buffer.read(), media_type="image/png") diff --git a/ballsdex/core/image_generator/image_gen.py b/ballsdex/core/image_generator/image_gen.py index 92ef06d3..514ed062 100755 --- a/ballsdex/core/image_generator/image_gen.py +++ b/ballsdex/core/image_generator/image_gen.py @@ -26,20 +26,22 @@ credits_font = ImageFont.truetype(str(SOURCES_PATH / "arial.ttf"), 40) -def draw_card(ball_instance: "BallInstance"): +def draw_card(ball_instance: "BallInstance", media_path: str = "./admin_panel/media/"): ball = ball_instance.countryball ball_health = (237, 115, 101, 255) ball_credits = ball.credits if special_image := ball_instance.special_card: - image = Image.open("." + special_image) + image = Image.open(media_path + special_image) if ball_instance.specialcard and ball_instance.specialcard.credits: ball_credits += f" • {ball_instance.specialcard.credits}" else: - image = Image.open("." + ball.cached_regime.background) + image = Image.open(media_path + ball.cached_regime.background) image = image.convert("RGBA") icon = ( - Image.open("." + ball.cached_economy.icon).convert("RGBA") if ball.cached_economy else None + Image.open(media_path + ball.cached_economy.icon).convert("RGBA") + if ball.cached_economy + else None ) draw = ImageDraw.Draw(image) @@ -95,7 +97,7 @@ def draw_card(ball_instance: "BallInstance"): stroke_fill=(255, 255, 255, 255), ) - artwork = Image.open("." + ball.collection_card).convert("RGBA") + artwork = Image.open(media_path + ball.collection_card).convert("RGBA") image.paste(ImageOps.fit(artwork, artwork_size), CORNERS[0]) # type: ignore if icon: diff --git a/ballsdex/core/models.py b/ballsdex/core/models.py index 1a71e5f3..6fcc86c3 100755 --- a/ballsdex/core/models.py +++ b/ballsdex/core/models.py @@ -8,7 +8,6 @@ import discord from discord.utils import format_dt -from fastapi_admin.models import AbstractAdmin from tortoise import exceptions, fields, models, signals, timezone, validators from tortoise.contrib.postgres.indexes import PostgreSQLIndex from tortoise.expressions import Q @@ -57,16 +56,6 @@ def __call__(self, value: int): raise exceptions.ValidationError("Discord IDs are between 17 and 19 characters long") -class User(AbstractAdmin): - last_login = fields.DatetimeField(description="Last Login", default=datetime.now) - avatar = fields.CharField(max_length=200, default="") - intro = fields.TextField(default="") - created_at = fields.DatetimeField(auto_now_add=True) - - def __str__(self): - return f"{self.pk}#{self.username}" - - class GuildConfig(models.Model): guild_id = fields.BigIntField( description="Discord guild ID", unique=True, validators=[DiscordSnowflakeValidator()] diff --git a/ballsdex/packages/admin/balls.py b/ballsdex/packages/admin/balls.py index c5657e3b..c4777d65 100644 --- a/ballsdex/packages/admin/balls.py +++ b/ballsdex/packages/admin/balls.py @@ -255,6 +255,11 @@ async def balls_info(self, interaction: discord.Interaction[BallsDexBot], countr if ball.catch_date and ball.spawned_time else "N/A" ) + admin_url = ( + f"[View online](<{settings.admin_url}/bd_models/ballinstance/{ball.pk}/change/>)" + if settings.admin_url + else "" + ) await interaction.response.send_message( f"**{settings.collectible_name.title()} ID:** {ball.pk}\n" f"**Player:** {ball.player}\n" @@ -268,7 +273,7 @@ async def balls_info(self, interaction: discord.Interaction[BallsDexBot], countr f"**Spawned at:** {spawned_time}\n" f"**Catch time:** {catch_time} seconds\n" f"**Caught in:** {ball.server_id if ball.server_id else 'N/A'}\n" - f"**Traded:** {ball.trade_player}\n", + f"**Traded:** {ball.trade_player}\n{admin_url}", ephemeral=True, ) await log_action(f"{interaction.user} got info for {ball}({ball.pk}).", interaction.client) @@ -589,9 +594,14 @@ async def balls_create( if wild_card: files.append(await wild_card.to_file()) await interaction.client.load_cache() + admin_url = ( + f"[View online](<{settings.admin_url}/bd_models/ball/{ball.pk}/change/>)\n" + if settings.admin_url + else "" + ) await interaction.followup.send( f"Successfully created a {settings.collectible_name} with ID {ball.pk}! " - "The internal cache was reloaded.\n" + f"The internal cache was reloaded.\n{admin_url}" f"{missing_default}\n" f"{name=} regime={regime.name} economy={economy.name if economy else None} " f"{health=} {attack=} {rarity=} {enabled=} {tradeable=} emoji={emoji}", diff --git a/ballsdex/packages/admin/blacklist.py b/ballsdex/packages/admin/blacklist.py index a1e55b45..6c9d8824 100644 --- a/ballsdex/packages/admin/blacklist.py +++ b/ballsdex/packages/admin/blacklist.py @@ -4,7 +4,13 @@ from tortoise.exceptions import DoesNotExist, IntegrityError from ballsdex.core.bot import BallsDexBot -from ballsdex.core.models import BlacklistedGuild, BlacklistedID, BlacklistHistory +from ballsdex.core.models import ( + BlacklistedGuild, + BlacklistedID, + BlacklistHistory, + GuildConfig, + Player, +) from ballsdex.core.utils.logging import log_action from ballsdex.core.utils.paginator import Pages from ballsdex.packages.admin.menu import BlacklistViewFormat @@ -124,18 +130,25 @@ async def blacklist_info( ) else: moderator_msg = "Moderator: Unknown" + if settings.admin_url and (player := await Player.get_or_none(discord_id=user.id)): + admin_url = ( + "\n[View history online]" + f"(<{settings.admin_url}/bd_models/player/{player.pk}/change/>)" + ) + else: + admin_url = "" if blacklisted.date: await interaction.response.send_message( f"`{user}` (`{user.id}`) was blacklisted on {format_dt(blacklisted.date)}" f"({format_dt(blacklisted.date, style='R')}) for the following reason:\n" - f"{blacklisted.reason}\n{moderator_msg}", + f"{blacklisted.reason}\n{moderator_msg}{admin_url}", ephemeral=True, ) else: await interaction.response.send_message( f"`{user}` (`{user.id}`) is currently blacklisted (date unknown)" " for the following reason:\n" - f"{blacklisted.reason}\n{moderator_msg}", + f"{blacklisted.reason}\n{moderator_msg}{admin_url}", ephemeral=True, ) @@ -331,17 +344,24 @@ async def blacklist_info_guild( ) else: moderator_msg = "Moderator: Unknown" + if settings.admin_url and (gconf := await GuildConfig.get_or_none(guild_id=guild.id)): + admin_url = ( + "\n[View history online]" + f"(<{settings.admin_url}/bd_models/guildconfig/{gconf.pk}/change/>)" + ) + else: + admin_url = "" if blacklisted.date: await interaction.response.send_message( f"`{guild}` (`{guild.id}`) was blacklisted on {format_dt(blacklisted.date)}" f"({format_dt(blacklisted.date, style='R')}) for the following reason:\n" - f"{blacklisted.reason}\n{moderator_msg}", + f"{blacklisted.reason}\n{moderator_msg}{admin_url}", ephemeral=True, ) else: await interaction.response.send_message( f"`{guild}` (`{guild.id}`) is currently blacklisted (date unknown)" " for the following reason:\n" - f"{blacklisted.reason}\n{moderator_msg}", + f"{blacklisted.reason}\n{moderator_msg}{admin_url}", ephemeral=True, ) diff --git a/ballsdex/packages/admin/history.py b/ballsdex/packages/admin/history.py index 010a626d..c4e58de8 100644 --- a/ballsdex/packages/admin/history.py +++ b/ballsdex/packages/admin/history.py @@ -2,10 +2,11 @@ import discord from discord import app_commands +from tortoise.exceptions import DoesNotExist from tortoise.expressions import Q from ballsdex.core.bot import BallsDexBot -from ballsdex.core.models import BallInstance, Trade +from ballsdex.core.models import BallInstance, Player, Trade from ballsdex.core.utils.paginator import Pages from ballsdex.packages.trade.display import TradeViewFormat, fill_trade_embed_fields from ballsdex.packages.trade.trade_user import TradingUser @@ -55,15 +56,21 @@ async def history_user( return queryset = Trade.all() - if user2: - queryset = queryset.filter( - (Q(player1__discord_id=user.id) & Q(player2__discord_id=user2.id)) - | (Q(player1__discord_id=user2.id) & Q(player2__discord_id=user.id)) - ) - else: - queryset = queryset.filter( - Q(player1__discord_id=user.id) | Q(player2__discord_id=user.id) - ) + try: + player1 = await Player.get(discord_id=user.id) + if user2: + player2 = await Player.get(discord_id=user2.id) + query = f"?q={user.id}+{user2.id}" + queryset = queryset.filter( + (Q(player1=player1) & Q(player2=player2)) + | (Q(player1=player2) & Q(player2=player1)) + ) + else: + query = f"?q={user.id}" + queryset = queryset.filter(Q(player1=player1) | Q(player2=player1)) + except DoesNotExist: + await interaction.followup.send("One or more players are not registered by the bot.") + return if days is not None and days > 0: end_date = datetime.datetime.now() @@ -82,7 +89,8 @@ async def history_user( f"History of {user.display_name} and {user2.display_name}:" ) - source = TradeViewFormat(history, user.display_name, interaction.client, True) + url = f"{settings.admin_url}/bd_models/trade/{query}" if settings.admin_url else None + source = TradeViewFormat(history, user.display_name, interaction.client, True, url) pages = Pages(source=source, interaction=interaction) await pages.start(ephemeral=True) @@ -152,8 +160,13 @@ async def history_ball( await interaction.followup.send("No history found.", ephemeral=True) return + url = ( + f"{settings.admin_url}/bd_models/ballinstance/{ball.pk}/change/" + if settings.admin_url + else None + ) source = TradeViewFormat( - trades, f"{settings.collectible_name} {ball}", interaction.client, True + trades, f"{settings.collectible_name} {ball}", interaction.client, True, url ) pages = Pages(source=source, interaction=interaction) await pages.start(ephemeral=True) @@ -188,6 +201,11 @@ async def trade_info( return embed = discord.Embed( title=f"Trade {trade.pk:0X}", + url=( + f"{settings.admin_url}/bd_models/trade/{trade.pk}/change/" + if settings.admin_url + else None + ), description=f"Trade ID: {trade.pk:0X}", timestamp=trade.date, ) diff --git a/ballsdex/packages/admin/info.py b/ballsdex/packages/admin/info.py index 275379ab..e6dc4c7d 100644 --- a/ballsdex/packages/admin/info.py +++ b/ballsdex/packages/admin/info.py @@ -56,8 +56,11 @@ async def guild( ) return + url = None if config := await GuildConfig.get_or_none(guild_id=guild.id): spawn_enabled = config.enabled and config.guild_id + if settings.admin_url: + url = f"{settings.admin_url}/bd_models/guildconfig/{config.pk}/change/" else: spawn_enabled = False @@ -69,12 +72,14 @@ async def guild( owner = await interaction.client.fetch_user(guild.owner_id) embed = discord.Embed( title=f"{guild.name} ({guild.id})", + url=url, description=f"**Owner:** {owner} ({guild.owner_id})", color=discord.Color.blurple(), ) else: embed = discord.Embed( title=f"{guild.name} ({guild.id})", + url=url, color=discord.Color.blurple(), ) embed.add_field(name="Members:", value=guild.member_count) @@ -115,12 +120,18 @@ async def user( if not player: await interaction.followup.send("The user you gave does not exist.", ephemeral=True) return + url = ( + f"{settings.admin_url}/bd_models/player/{player.pk}/change/" + if settings.admin_url + else None + ) total_user_balls = await BallInstance.filter( catch_date__gte=datetime.datetime.now() - datetime.timedelta(days=days), player=player, ) embed = discord.Embed( title=f"{user} ({user.id})", + url=url, description=( f"**Privacy Policy:** {PRIVATE_POLICY_MAP[player.privacy_policy]}\n" f"**Donation Policy:** {DONATION_POLICY_MAP[player.donation_policy]}\n" diff --git a/ballsdex/packages/admin/menu.py b/ballsdex/packages/admin/menu.py index f6e01023..bc80c403 100644 --- a/ballsdex/packages/admin/menu.py +++ b/ballsdex/packages/admin/menu.py @@ -3,9 +3,10 @@ import discord from discord.utils import format_dt -from ballsdex.core.models import BlacklistHistory +from ballsdex.core.models import BlacklistHistory, Player from ballsdex.core.utils import menus from ballsdex.core.utils.paginator import Pages +from ballsdex.settings import settings if TYPE_CHECKING: from ballsdex.core.bot import BallsDexBot @@ -35,6 +36,13 @@ async def format_page(self, menu: Pages, blacklist: BlacklistHistory) -> discord inline=True, ) embed.add_field(name="Action Time", value=format_dt(blacklist.date, "R"), inline=True) + if settings.admin_url and (player := await Player.get_or_none(discord_id=self.header)): + embed.add_field( + name="\u200B", + value="[View history online]" + f"(<{settings.admin_url}/bd_models/player/{player.pk}/change/>)", + inline=False, + ) embed.set_footer( text=( f"Blacklist History {menu.current_page + 1}/{menu.source.get_max_pages()}" diff --git a/ballsdex/packages/countryballs/countryball.py b/ballsdex/packages/countryballs/countryball.py index 9520e941..a974f745 100755 --- a/ballsdex/packages/countryballs/countryball.py +++ b/ballsdex/packages/countryballs/countryball.py @@ -55,7 +55,7 @@ def generate_random_name(): return "".join(random.choices(source, k=15)) extension = self.model.wild_card.split(".")[-1] - file_location = "." + self.model.wild_card + file_location = "./admin_panel/media/" + self.model.wild_card file_name = f"nt_{generate_random_name()}.{extension}" try: permissions = channel.permissions_for(channel.guild.me) diff --git a/ballsdex/packages/trade/display.py b/ballsdex/packages/trade/display.py index 446aef4d..0c218f9a 100644 --- a/ballsdex/packages/trade/display.py +++ b/ballsdex/packages/trade/display.py @@ -18,8 +18,10 @@ def __init__( header: str, bot: "BallsDexBot", is_admin: bool = False, + url: str | None = None, ): self.header = header + self.url = url self.bot = bot self.is_admin = is_admin super().__init__(entries, per_page=1) @@ -28,6 +30,7 @@ async def format_page(self, menu: Pages, trade: TradeModel) -> discord.Embed: embed = discord.Embed( title=f"Trade history for {self.header}", description=f"Trade ID: {trade.pk:0X}", + url=self.url if self.is_admin else None, timestamp=trade.date, ) embed.set_footer( diff --git a/ballsdex/settings.py b/ballsdex/settings.py index 113410a7..6f9a2fdc 100644 --- a/ballsdex/settings.py +++ b/ballsdex/settings.py @@ -60,6 +60,12 @@ class Settings: List of packages the bot will load upon startup spawn_manager: str Python path to a class implementing `BaseSpawnManager`, handling cooldowns and anti-cheat + webhook_url: str | None + URL of a Discord webhook for admin notifications + client_id: str + ID of the Discord application + client_secret: str + Secret key of the Discord application (not the bot token) """ bot_token: str = "" @@ -102,6 +108,12 @@ class Settings: spawn_manager: str = "ballsdex.packages.countryballs.spawn.SpawnManager" + # django admin panel + webhook_url: str | None = None + admin_url: str | None = None + client_id: str = "" + client_secret: str = "" + settings = Settings() @@ -156,6 +168,13 @@ def read_settings(path: "Path"): settings.spawn_manager = content.get( "spawn-manager", "ballsdex.packages.countryballs.spawn.SpawnManager" ) + + if admin := content.get("admin-panel"): + settings.webhook_url = admin.get("webhook-url") + settings.client_id = admin.get("client-id") + settings.client_secret = admin.get("client-secret") + settings.admin_url = admin.get("url") + log.info("Settings loaded.") @@ -241,6 +260,23 @@ def write_default_settings(path: "Path"): # a list of IDs that must be considered owners in addition to the application/team owner co-owners: + +# Admin panel related settings +admin-panel: + + # to enable Discord OAuth2 login, fill this + # client ID of the Discord application (not the bot's user ID) + client-id: + # client secret of the Discord application (this is not the bot token) + client-secret: + + # to get admin notifications from the admin panel, create a Discord webhook and paste the url + webhook-url: + + # this will provide some hyperlinks to the admin panel when using /admin commands + # set to an empty string to disable those links entirely + url: http://localhost:8000 + # list of packages that will be loaded packages: - ballsdex.packages.admin @@ -273,6 +309,7 @@ def update_settings(path: "Path"): add_plural_collectible = "plural-collectible-name" not in content add_packages = "packages:" not in content add_spawn_manager = "spawn-manager" not in content + add_django = "Admin panel related settings" not in content for line in content.splitlines(): if line.startswith("owners:"): @@ -338,6 +375,26 @@ def update_settings(path: "Path"): content += """ # define a custom spawn manager implementation spawn-manager: ballsdex.packages.countryballs.spawn.SpawnManager +""" + + if add_django: + content += """ +# Admin panel related settings +admin-panel: + + # to enable Discord OAuth2 login, fill this + # client ID of the Discord application (not the bot's user ID) + client-id: + # client secret of the Discord application (this is not the bot token) + client-secret: + + # to get admin notifications from the admin panel, create a Discord webhook and paste the url + webhook-url: + + # this will provide some hyperlinks to the admin panel when using /admin commands + # set to an empty string to disable those links entirely + url: http://localhost:8000 + """ if any( @@ -350,6 +407,7 @@ def update_settings(path: "Path"): add_plural_collectible, add_packages, add_spawn_manager, + add_django, ) ): path.write_text(content) diff --git a/docker-compose.yml b/docker-compose.yml index cdbc7c8a..7a5fa700 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,22 +29,28 @@ services: admin-panel: image: ballsdex + build: . ports: - "8000:8000" networks: - internal environment: - *postgres-url - - "BALLSDEXBOT_REDIS_URL=redis://redis" + # if serving the admin panel online, copy the file "production.example.py" and uncomment + # - DJANGO_SETTINGS_MODULE="admin_panel.settings.production" depends_on: - postgres-db - - redis-cache volumes: - type: bind source: ./ target: /code tty: true - command: poetry run uvicorn ballsdex.core.admin:_app --host 0.0.0.0 + working_dir: /code/admin_panel + command: > + bash -c " + poetry run python3 manage.py migrate --no-input && + poetry run python3 manage.py collectstatic --no-input && + poetry run uvicorn admin_panel.asgi:application --host 0.0.0.0" postgres-db: image: postgres diff --git a/migrations/models/38_20250120182312_update.sql b/migrations/models/38_20250120182312_update.sql new file mode 100644 index 00000000..2d584342 --- /dev/null +++ b/migrations/models/38_20250120182312_update.sql @@ -0,0 +1,13 @@ +-- upgrade -- +DROP TABLE IF EXISTS "user"; +-- downgrade -- +CREATE TABLE IF NOT EXISTS "user" ( + "id" SERIAL NOT NULL PRIMARY KEY, + "username" VARCHAR(50) NOT NULL UNIQUE, + "password" VARCHAR(200) NOT NULL, + "last_login" TIMESTAMPTZ NOT NULL, + "avatar" VARCHAR(200) NOT NULL DEFAULT '', + "intro" TEXT NOT NULL, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +COMMENT ON COLUMN "user"."last_login" IS 'Last Login'; diff --git a/poetry.lock b/poetry.lock index 1541b112..15c47aa4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -23,18 +23,6 @@ tortoise-orm = "*" asyncmy = ["asyncmy"] asyncpg = ["asyncpg"] -[[package]] -name = "aiofiles" -version = "24.1.0" -description = "File support for asyncio." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, - {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, -] - [[package]] name = "aiohappyeyeballs" version = "2.4.4" @@ -193,14 +181,14 @@ files = [ [[package]] name = "anyio" -version = "4.7.0" +version = "4.8.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, - {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, ] [package.dependencies] @@ -210,7 +198,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -225,6 +213,21 @@ files = [ {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] +[[package]] +name = "asgiref" +version = "3.8.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + [[package]] name = "asyncpg" version = "0.30.0" @@ -353,60 +356,6 @@ files = [ {file = "audioop_lts-0.2.1.tar.gz", hash = "sha256:e81268da0baa880431b68b1308ab7257eb33f356e57a5f9b1f915dfb13dd1387"}, ] -[[package]] -name = "babel" -version = "2.16.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, - {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, -] - -[package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] - -[[package]] -name = "bcrypt" -version = "4.2.1" -description = "Modern password hashing for your software and your servers" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139"}, - {file = "bcrypt-4.2.1-cp37-abi3-win32.whl", hash = "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005"}, - {file = "bcrypt-4.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526"}, - {file = "bcrypt-4.2.1-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d"}, - {file = "bcrypt-4.2.1-cp39-abi3-win32.whl", hash = "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf"}, - {file = "bcrypt-4.2.1-cp39-abi3-win_amd64.whl", hash = "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c"}, - {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e"}, - {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f"}, - {file = "bcrypt-4.2.1.tar.gz", hash = "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe"}, -] - -[package.extras] -tests = ["pytest (>=3.2.1,!=3.3.0)"] -typecheck = ["mypy"] - [[package]] name = "black" version = "24.8.0" @@ -465,6 +414,99 @@ files = [ {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, ] +[[package]] +name = "certifi" +version = "2024.12.14" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "cfgv" version = "3.4.0" @@ -478,6 +520,108 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "charset-normalizer" +version = "3.4.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +] + [[package]] name = "click" version = "8.1.7" @@ -506,6 +650,68 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "cryptography" +version = "44.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +groups = ["main"] +files = [ + {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, + {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, + {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, + {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, + {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, + {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, + {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + [[package]] name = "dictdiffer" version = "0.9.0" @@ -559,52 +765,122 @@ files = [ ] [[package]] -name = "fastapi" -version = "0.115.4" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +name = "dj-database-url" +version = "2.3.0" +description = "Use Database URLs in your Django Application." optional = false -python-versions = ">=3.8" +python-versions = "*" +groups = ["main"] +files = [ + {file = "dj_database_url-2.3.0-py3-none-any.whl", hash = "sha256:bb0d414ba0ac5cd62773ec7f86f8cc378a9dbb00a80884c2fc08cc570452521e"}, + {file = "dj_database_url-2.3.0.tar.gz", hash = "sha256:ae52e8e634186b57e5a45e445da5dc407a819c2ceed8a53d1fac004cc5288787"}, +] + +[package.dependencies] +Django = ">=4.2" +typing_extensions = ">=3.10.0.0" + +[[package]] +name = "django" +version = "5.1.4" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "fastapi-0.115.4-py3-none-any.whl", hash = "sha256:0b504a063ffb3cf96a5e27dc1bc32c80ca743a2528574f9cdc77daa2d31b4742"}, - {file = "fastapi-0.115.4.tar.gz", hash = "sha256:db653475586b091cb8b2fec2ac54a680ac6a158e07406e1abae31679e8826349"}, + {file = "Django-5.1.4-py3-none-any.whl", hash = "sha256:236e023f021f5ce7dee5779de7b286565fdea5f4ab86bae5338e3f7b69896cf0"}, + {file = "Django-5.1.4.tar.gz", hash = "sha256:de450c09e91879fa5a307f696e57c851955c910a438a35e6b4c895e86bedc82a"}, ] [package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.40.0,<0.42.0" -typing-extensions = ">=4.8.0" +asgiref = ">=3.8.1,<4" +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] [[package]] -name = "fastapi-admin" -version = "1.0.5" -description = "A fast admin dashboard based on FastAPI and TortoiseORM with tabler ui, inspired by Django admin." +name = "django-admin-action-forms" +version = "1.3.0" +description = "Extension for the Django admin panel that allows passing additional parameters to actions by creating intermediate pages with forms." optional = false -python-versions = "^3.7" +python-versions = "*" groups = ["main"] -files = [] -develop = false +files = [ + {file = "django_admin_action_forms-1.3.0-py3-none-any.whl", hash = "sha256:e614303507f0d2bc3d47a14da3a1bcf0265f69e150c453e21a659bdf7677e0de"}, + {file = "django_admin_action_forms-1.3.0.tar.gz", hash = "sha256:a9a802217be67b26ffef2f82da6b22fa57c80e44fa4c87c79750dcf71f9b5b0e"}, +] [package.dependencies] -aiofiles = "*" -Babel = "*" -bcrypt = "*" -fastapi = "*" -jinja2 = "*" -python-multipart = "*" -redis = "^4.2.0rc1" -tortoise-orm = "*" -uvicorn = {version = "*", extras = ["standard"]} +django = ">=3.2" -[package.source] -type = "git" -url = "https://github.com/Ballsdex-Team/fastapi-admin" -reference = "ballsdex" -resolved_reference = "eeb4d0c99f3b5a3558af074fa9fafb05bcc140e9" +[[package]] +name = "django-admin-autocomplete-filter" +version = "0.7.1" +description = "A simple Django app to render list filters in django admin using autocomplete widget" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "django-admin-autocomplete-filter-0.7.1.tar.gz", hash = "sha256:5a8c9a7016e03104627b80b40811dcc567f26759971e4407f933951546367ba0"}, + {file = "django_admin_autocomplete_filter-0.7.1-py3-none-any.whl", hash = "sha256:b2a71be2c4a68f828289eb51f71316dbdfac00a1843f53df1fbe4767aad2e3c0"}, +] + +[package.dependencies] +django = ">=2.0" + +[[package]] +name = "django-admin-inline-paginator" +version = "0.4.0" +description = "The \"Django Admin Inline Paginator\" is simple way to paginate your inline in django admin" +optional = false +python-versions = ">=3.3" +groups = ["main"] +files = [ + {file = "django-admin-inline-paginator-0.4.0.tar.gz", hash = "sha256:e1558b468d347e965c4d25be7f546da6682ea03d95098ae37b90c926edd491b1"}, + {file = "django_admin_inline_paginator-0.4.0-py3-none-any.whl", hash = "sha256:63b2e13dca6b0933f4b7a4d3262c7cb31db8cda19ad005bcbc1c29bba0e95a1a"}, +] + +[package.dependencies] +django = "*" + +[package.extras] +code-quality = ["bandit", "isort", "xenon"] +dev = ["django", "django-mock-queries", "pylint", "pytest", "pytest-cov", "pytest-pylint", "pytest-watch", "tox"] + +[[package]] +name = "django-debug-toolbar" +version = "4.4.6" +description = "A configurable set of panels that display various debug information about the current request/response." +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "django_debug_toolbar-4.4.6-py3-none-any.whl", hash = "sha256:3beb671c9ec44ffb817fad2780667f172bd1c067dbcabad6268ce39a81335f45"}, + {file = "django_debug_toolbar-4.4.6.tar.gz", hash = "sha256:36e421cb908c2f0675e07f9f41e3d1d8618dc386392ec82d23bcfcd5d29c7044"}, +] + +[package.dependencies] +django = ">=4.2.9" +sqlparse = ">=0.2" + +[[package]] +name = "django-nonrelated-inlines" +version = "0.2" +description = "Django admin inlines for unrelated models" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "django-nonrelated-inlines-0.2.tar.gz", hash = "sha256:e123010a3ad18b049781d6688b3ff45b2441e69fd63802bec94c37cd36c83611"}, + {file = "django_nonrelated_inlines-0.2-py3-none-any.whl", hash = "sha256:216a4513971c58568f450a2339064746624de718a946088534e86011b400ccc3"}, +] + +[package.dependencies] +Django = ">=2.0" [[package]] name = "filelock" @@ -909,24 +1185,6 @@ docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alab qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] -[[package]] -name = "jinja2" -version = "3.1.4" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - [[package]] name = "markdown-it-py" version = "3.0.0" @@ -952,77 +1210,6 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - [[package]] name = "mccabe" version = "0.7.0" @@ -1176,6 +1363,23 @@ files = [ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + [[package]] name = "packaging" version = "24.2" @@ -1476,6 +1680,104 @@ files = [ {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, ] +[[package]] +name = "psycopg" +version = "3.2.3" +description = "PostgreSQL database adapter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907"}, + {file = "psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (==3.2.3)"] +c = ["psycopg-c (==3.2.3)"] +dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.11)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] +pool = ["psycopg-pool"] +test = ["anyio (>=4.0)", "mypy (>=1.11)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + +[[package]] +name = "psycopg-binary" +version = "3.2.3" +description = "PostgreSQL database adapter for Python -- C optimisation distribution" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:965455eac8547f32b3181d5ec9ad8b9be500c10fe06193543efaaebe3e4ce70c"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:71adcc8bc80a65b776510bc39992edf942ace35b153ed7a9c6c573a6849ce308"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f73adc05452fb85e7a12ed3f69c81540a8875960739082e6ea5e28c373a30774"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8630943143c6d6ca9aefc88bbe5e76c90553f4e1a3b2dc339e67dc34aa86f7e"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bffb61e198a91f712cc3d7f2d176a697cb05b284b2ad150fb8edb308eba9002"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4fa2240c9fceddaa815a58f29212826fafe43ce80ff666d38c4a03fb036955"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:192a5f8496e6e1243fdd9ac20e117e667c0712f148c5f9343483b84435854c78"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64dc6e9ec64f592f19dc01a784e87267a64a743d34f68488924251253da3c818"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:79498df398970abcee3d326edd1d4655de7d77aa9aecd578154f8af35ce7bbd2"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:949551752930d5e478817e0b49956350d866b26578ced0042a61967e3fcccdea"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:80a2337e2dfb26950894c8301358961430a0304f7bfe729d34cc036474e9c9b1"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:6d8f2144e0d5808c2e2aed40fbebe13869cd00c2ae745aca4b3b16a435edb056"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94253be2b57ef2fea7ffe08996067aabf56a1eb9648342c9e3bad9e10c46e045"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fda0162b0dbfa5eaed6cdc708179fa27e148cb8490c7d62e5cf30713909658ea"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c0419cdad8c70eaeb3116bb28e7b42d546f91baf5179d7556f230d40942dc78"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74fbf5dd3ef09beafd3557631e282f00f8af4e7a78fbfce8ab06d9cd5a789aae"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d784f614e4d53050cbe8abf2ae9d1aaacf8ed31ce57b42ce3bf2a48a66c3a5c"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4e76ce2475ed4885fe13b8254058be710ec0de74ebd8ef8224cf44a9a3358e5f"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5938b257b04c851c2d1e6cb2f8c18318f06017f35be9a5fe761ee1e2e344dfb7"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:257c4aea6f70a9aef39b2a77d0658a41bf05c243e2bf41895eb02220ac6306f3"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:06b5cc915e57621eebf2393f4173793ed7e3387295f07fed93ed3fb6a6ccf585"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:09baa041856b35598d335b1a74e19a49da8500acedf78164600694c0ba8ce21b"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:48f8ca6ee8939bab760225b2ab82934d54330eec10afe4394a92d3f2a0c37dd6"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5361ea13c241d4f0ec3f95e0bf976c15e2e451e9cc7ef2e5ccfc9d170b197a40"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb987f14af7da7c24f803111dbc7392f5070fd350146af3345103f76ea82e339"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0463a11b1cace5a6aeffaf167920707b912b8986a9c7920341c75e3686277920"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b7be9a6c06518967b641fb15032b1ed682fd3b0443f64078899c61034a0bca6"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64a607e630d9f4b2797f641884e52b9f8e239d35943f51bef817a384ec1678fe"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fa33ead69ed133210d96af0c63448b1385df48b9c0247eda735c5896b9e6dbbf"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1f8b0d0e99d8e19923e6e07379fa00570be5182c201a8c0b5aaa9a4d4a4ea20b"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:709447bd7203b0b2debab1acec23123eb80b386f6c29e7604a5d4326a11e5bd6"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e37d5027e297a627da3551a1e962316d0f88ee4ada74c768f6c9234e26346d9"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:261f0031ee6074765096a19b27ed0f75498a8338c3dcd7f4f0d831e38adf12d1"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:41fdec0182efac66b27478ac15ef54c9ebcecf0e26ed467eb7d6f262a913318b"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:07d019a786eb020c0f984691aa1b994cb79430061065a694cf6f94056c603d26"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c57615791a337378fe5381143259a6c432cdcbb1d3e6428bfb7ce59fff3fb5c"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8eb9a4e394926b93ad919cad1b0a918e9b4c846609e8c1cfb6b743683f64da0"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5905729668ef1418bd36fbe876322dcb0f90b46811bba96d505af89e6fbdce2f"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd65774ed7d65101b314808b6893e1a75b7664f680c3ef18d2e5c84d570fa393"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:700679c02f9348a0d0a2adcd33a0275717cd0d0aee9d4482b47d935023629505"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:96334bb64d054e36fed346c50c4190bad9d7c586376204f50bede21a913bf942"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9099e443d4cc24ac6872e6a05f93205ba1a231b1a8917317b07c9ef2b955f1f4"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1985ab05e9abebfbdf3163a16ebb37fbc5d49aff2bf5b3d7375ff0920bbb54cd"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:e90352d7b610b4693fad0feea48549d4315d10f1eba5605421c92bb834e90170"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:69320f05de8cdf4077ecd7fefdec223890eea232af0d58f2530cbda2871244a0"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4926ea5c46da30bec4a85907aa3f7e4ea6313145b2aa9469fdb861798daf1502"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c64c4cd0d50d5b2288ab1bcb26c7126c772bbdebdfadcd77225a77df01c4a57e"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05a1bdce30356e70a05428928717765f4a9229999421013f41338d9680d03a63"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad357e426b0ea5c3043b8ec905546fa44b734bf11d33b3da3959f6e4447d350"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:967b47a0fd237aa17c2748fdb7425015c394a6fb57cdad1562e46a6eb070f96d"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:71db8896b942770ed7ab4efa59b22eee5203be2dfdee3c5258d60e57605d688c"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2773f850a778575dd7158a6dd072f7925b67f3ba305e2003538e8831fec77a1d"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aeddf7b3b3f6e24ccf7d0edfe2d94094ea76b40e831c16eff5230e040ce3b76b"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:824c867a38521d61d62b60aca7db7ca013a2b479e428a0db47d25d8ca5067410"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:9994f7db390c17fc2bd4c09dca722fd792ff8a49bb3bdace0c50a83f22f1767d"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1303bf8347d6be7ad26d1362af2c38b3a90b8293e8d56244296488ee8591058e"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:842da42a63ecb32612bb7f5b9e9f8617eab9bc23bd58679a441f4150fcc51c96"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bb342a01c76f38a12432848e6013c57eb630103e7556cf79b705b53814c3949"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd40af959173ea0d087b6b232b855cfeaa6738f47cb2a0fd10a7f4fa8b74293f"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9b60b465773a52c7d4705b0a751f7f1cdccf81dd12aee3b921b31a6e76b07b0e"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fc6d87a1c44df8d493ef44988a3ded751e284e02cdf785f746c2d357e99782a6"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f0b018e37608c3bfc6039a1dc4eb461e89334465a19916be0153c757a78ea426"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a29f5294b0b6360bfda69653697eff70aaf2908f58d1073b0acd6f6ab5b5a4f"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:e56b1fd529e5dde2d1452a7d72907b37ed1b4f07fdced5d8fb1e963acfff6749"}, +] + [[package]] name = "ptpython" version = "3.0.29" @@ -1511,6 +1813,19 @@ files = [ {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, ] +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pydantic" version = "2.10.3" @@ -1673,6 +1988,103 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyinstrument" +version = "5.0.0" +description = "Call stack profiler for Python. Shows you why your code is slow!" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pyinstrument-5.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6a83cf18f5594e1b1899b12b46df7aabca556eef895846ccdaaa3a46a37d1274"}, + {file = "pyinstrument-5.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1cc236313272d0222261be8e2b2a08e42d7ccbe54db9059babf4d77040da1880"}, + {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dd685d68a31f3715ca61f82c37c1c2f8b75f45646bd9840e04681d91862bd85"}, + {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cecd0f6558f13fba74a9f036b2b168956206e9525dcb84c6add2d73ab61dc22"}, + {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a8485c2e41082a20822001a6651667bb5327f6f5f6759987198593e45bb376"}, + {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a6294b7111348765ba4c311fc91821ed8b59c6690c4dab23aa7165a67da9e972"}, + {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a164f3dae5c7db2faa501639659d64034cde8db62a4d6744712593a369bc8629"}, + {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f6bac8a434407de6f2ebddbcdecdb19b324c9315cbb8b8c2352714f7ced8181"}, + {file = "pyinstrument-5.0.0-cp310-cp310-win32.whl", hash = "sha256:7e8dc887e535f5c5e5a2a64a0729496f11ddcef0c23b0a555d5ab6fa19759445"}, + {file = "pyinstrument-5.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c337190a1818841732643ba93065411591df526bc9de44b97ba8f56b581d2ef"}, + {file = "pyinstrument-5.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c9052f548ec5ccecc50676fbf1a1d0b60bdbd3cd67630c5253099af049d1f0ad"}, + {file = "pyinstrument-5.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:197d25487f52da3f8ec26d46db7202bc5d703cc73c1503371166417eb7cea14e"}, + {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a072d928dc16a32e0f3d1e51726f4472a69d66d838ee1d1bf248737fd70b9415"}, + {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2c7ae2c984879a645fce583bf3053b7e57495f60c1e158bb71ad7dfced1fbf1"}, + {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8284bf8847629c9a5054702b9306eab3ab14c2474959e01e606369ffbcf938bc"}, + {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fd94cc725efb1dd41ae8e20a5f06a6a5363dec959e8a9dacbac3f4d12d28f03"}, + {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e0fdb9fe6f9c694940410dcc82e23a3fe2928114328efd35047fc0bb8a6c959f"}, + {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ffe938e63173ceb8ce7b6b309ce26c9d44d16f53c0162d89d6e706eb9e69802"}, + {file = "pyinstrument-5.0.0-cp311-cp311-win32.whl", hash = "sha256:80d2a248516f372a89e0fe9ddf4a9d6388a4c6481b6ebd3dfe01b3cd028c0275"}, + {file = "pyinstrument-5.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:7ccf4267aff62de0e1d976e8f5da25dcb69737ae86e38d3cfffa24877837e7d1"}, + {file = "pyinstrument-5.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:dec3529a5351ea160baeef1ef2a6e28b1a7a7b3fb5e9863fae8de6da73d0f69a"}, + {file = "pyinstrument-5.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5a39e3ef84c56183f8274dfd584b8c2fae4783c6204f880513e70ab2440b9137"}, + {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3938f063ee065e05826628dadf1fb32c7d26b22df4a945c22f7fe25ea1ba6a2"}, + {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18990cc16b2e23b54738aa2f222863e1d36daaaec8f67b1613ddfa41f5b24db"}, + {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3731412b5bfdcef8014518f145140c69384793e218863a33a39ccfe5fb42045"}, + {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02b2eaf38460b14eea646d6bb7f373eb5bb5691d13f788e80bdcb3a4eaa2519e"}, + {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e57db06590f13657b2bce8c4d9cf8e9e2bd90bb729bcbbe421c531ba67ad7add"}, + {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddaa3001c1b798ec9bf1266ef476bbc0834b74d547d531f5ed99e7d05ac5d81b"}, + {file = "pyinstrument-5.0.0-cp312-cp312-win32.whl", hash = "sha256:b69ff982acf5ef2f4e0f32ce9b4b598f256faf88438f233ea3a72f1042707e5b"}, + {file = "pyinstrument-5.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:0bf4ef061d60befe72366ce0ed4c75dee5be089644de38f9936d2df0bcf44af0"}, + {file = "pyinstrument-5.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79a54def2d4aa83a4ed37c6cffc5494ae5de140f0453169eb4f7c744cc249d3a"}, + {file = "pyinstrument-5.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9538f746f166a40c8802ebe5c3e905d50f3faa189869cd71c083b8a639e574bb"}, + {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bbab65cae1483ad8a18429511d1eac9e3efec9f7961f2fd1bf90e1e2d69ef15"}, + {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4351ad041d208c597e296a0e9c2e6e21cc96804608bcafa40cfa168f3c2b8f79"}, + {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceee5252f4580abec29bcc5c965453c217b0d387c412a5ffb8afdcda4e648feb"}, + {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b3050a4e7033103a13cfff9802680e2070a9173e1a258fa3f15a80b4eb9ee278"}, + {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3b1f44a34da7810938df615fb7cbc43cd879b42ca6b5cd72e655aee92149d012"}, + {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fde075196c8a3b2be191b8da05b92ff909c78d308f82df56d01a8cfdd6da07b9"}, + {file = "pyinstrument-5.0.0-cp313-cp313-win32.whl", hash = "sha256:1a9b62a8b54e05e7723eb8b9595fadc43559b73290c87b3b1cb2dc5944559790"}, + {file = "pyinstrument-5.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2478d2c55f77ad8e281e67b0dfe7c2176304bb824c307e86e11890f5e68d7feb"}, + {file = "pyinstrument-5.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c2e3b4283f85232fd5818e2153e6798bceb39a8c3ccfaa22fae08faf554740b7"}, + {file = "pyinstrument-5.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fb1139d2822abff1cbf1c81c018341f573b7afa23a94ce74888a0f6f47828cbc"}, + {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c971566d86ba46a7233d3f5b0d85d7ee4c9863f541f5d8f796c3947ebe17f68"}, + {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:429376235960179d6ab9b97e7871090059d39de160b4e3b2723672f30e8eea8e"}, + {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8599b4b0630c776b30fc3c4f7476d5e3814ee7fe42d99131644fe3c00b40fdf1"}, + {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc688afa2a5368042a7cb56866d5a28fdff8f37a282f7be79b17cae042841b"}, + {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5d34c06e2276d1f549a540bccb063688ea3d876e6df7c391205f1c8b4b96d5c8"}, + {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d3b2ec6e028731dbb2ba8cf06f19030162789e6696bca990a09519881ad42fb"}, + {file = "pyinstrument-5.0.0-cp38-cp38-win32.whl", hash = "sha256:5ed6f5873a7526ec5915e45d956d044334ef302653cf63649e48c41561aaa285"}, + {file = "pyinstrument-5.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:9e87d65bae7d0f5ef50908e35d67d43b7cc566909995cc99e91721bb49b4ea06"}, + {file = "pyinstrument-5.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bd953163616bc29c2ccb1e4c0e48ccdd11e0a97fc849da26bc362bba372019ba"}, + {file = "pyinstrument-5.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d2a7279ed9b6d7cdae247bc2e57095a32f35dfe32182c334ab0ac3eb02e0eac"}, + {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68001dfcb8a37b624a1c3de5d2ee7d634f63eac7a6dd1357b7370a5cdbdcf567"}, + {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c4c3cc6410ad5afe0e352a7fb09fb1ab85eb5676ec5ec8522123759d9cc68f"}, + {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d87ddab66b1b3525ad3abc49a88aaa51efcaf83578e9d2a702c03a1cea39f28"}, + {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03182ffaa9c91687cbaba80dc0c5a47015c5ea170fe642f632d88e885cf07356"}, + {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:39b60417c9c12eed04e1886644e92aa0b281d72e5d0b097b16253cade43110f7"}, + {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7bb389b6d1573361bd1367b296133c5c69184e35fc18db22e29e8cdf56f158f9"}, + {file = "pyinstrument-5.0.0-cp39-cp39-win32.whl", hash = "sha256:ae69478815edb3c63e7ebf82e1e13e38c3fb2bab833b1c013643c3475b1b8cf5"}, + {file = "pyinstrument-5.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:83caeb4150c0334e9e290c0f9bb164ff6bdc199065ecb62016268e8a88589a51"}, + {file = "pyinstrument-5.0.0.tar.gz", hash = "sha256:144f98eb3086667ece461f66324bf1cc1ee0475b399ab3f9ded8449cc76b7c90"}, +] + +[package.extras] +bin = ["click", "nox"] +docs = ["furo (==2024.7.18)", "myst-parser (==3.0.1)", "sphinx (==7.4.7)", "sphinx-autobuild (==2024.4.16)", "sphinxcontrib-programoutput (==0.17)"] +examples = ["django", "litestar", "numpy"] +test = ["cffi (>=1.17.0)", "flaky", "greenlet (>=3)", "ipython", "pytest", "pytest-asyncio (==0.23.8)", "trio"] +types = ["typing-extensions"] + +[[package]] +name = "pyjwt" +version = "2.10.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + [[package]] name = "pypika-tortoise" version = "0.3.2" @@ -1687,23 +2099,25 @@ files = [ [[package]] name = "pyright" -version = "1.1.335" +version = "1.1.390" description = "Command line wrapper for pyright" optional = true python-versions = ">=3.7" groups = ["main"] markers = "extra == \"dev\"" files = [ - {file = "pyright-1.1.335-py3-none-any.whl", hash = "sha256:1149d99d5cea3997010a5ac39611534e0426125d5090913ae5cb1e0e2c9fbca3"}, - {file = "pyright-1.1.335.tar.gz", hash = "sha256:12c09c1644b223515cc342f7d383e55eefeedd730d7875e39a2cf338c2d99be4"}, + {file = "pyright-1.1.390-py3-none-any.whl", hash = "sha256:ecebfba5b6b50af7c1a44c2ba144ba2ab542c227eb49bc1f16984ff714e0e110"}, + {file = "pyright-1.1.390.tar.gz", hash = "sha256:aad7f160c49e0fbf8209507a15e17b781f63a86a1facb69ca877c71ef2e9538d"}, ] [package.dependencies] nodeenv = ">=1.6.0" +typing-extensions = ">=4.1" [package.extras] -all = ["twine (>=3.4.1)"] +all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] dev = ["twine (>=3.4.1)"] +nodejs = ["nodejs-wheel-binaries"] [[package]] name = "python-dateutil" @@ -1736,17 +2150,24 @@ files = [ cli = ["click (>=5.0)"] [[package]] -name = "python-multipart" -version = "0.0.19" -description = "A streaming multipart parser for Python" +name = "python3-openid" +version = "3.2.0" +description = "OpenID support for modern servers and consumers." optional = false -python-versions = ">=3.8" +python-versions = "*" groups = ["main"] files = [ - {file = "python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d"}, - {file = "python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc"}, + {file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"}, + {file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"}, ] +[package.dependencies] +defusedxml = "*" + +[package.extras] +mysql = ["mysql-connector-python"] +postgresql = ["psycopg2"] + [[package]] name = "pytz" version = "2024.2" @@ -1823,20 +2244,45 @@ files = [ ] [[package]] -name = "redis" -version = "4.6.0" -description = "Python client for Redis database and key-value store" +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"}, - {file = "redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + [package.extras] -hiredis = ["hiredis (>=1.0.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=3.4" +groups = ["main"] +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "rich" @@ -1882,22 +2328,63 @@ files = [ ] [[package]] -name = "starlette" -version = "0.41.3" -description = "The little ASGI library that shines." +name = "social-auth-app-django" +version = "5.4.2" +description = "Python Social Authentication, Django integration." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7"}, - {file = "starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835"}, + {file = "social-auth-app-django-5.4.2.tar.gz", hash = "sha256:c8832c6cf13da6ad76f5613bcda2647d89ae7cfbc5217fadd13477a3406feaa8"}, + {file = "social_auth_app_django-5.4.2-py3-none-any.whl", hash = "sha256:0c041a31707921aef9a930f143183c65d8c7b364381364a50f3f7c6fcc9d62f6"}, ] [package.dependencies] -anyio = ">=3.4.0,<5" +Django = ">=3.2" +social-auth-core = ">=4.4.1" + +[[package]] +name = "social-auth-core" +version = "4.5.4" +description = "Python social authentication made simple." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "social-auth-core-4.5.4.tar.gz", hash = "sha256:d3dbeb0999ffd0e68aa4bd73f2ac698a18133fd11b3fc890e1366f18c8889fac"}, + {file = "social_auth_core-4.5.4-py3-none-any.whl", hash = "sha256:33cf970a623c442376f9d4a86fb187579e4438649daa5b5be993d05e74d7b2db"}, +] + +[package.dependencies] +cryptography = ">=1.4" +defusedxml = ">=0.5.0rc1" +oauthlib = ">=1.0.3" +PyJWT = ">=2.7.0" +python3-openid = ">=3.0.10" +requests = ">=2.9.1" +requests-oauthlib = ">=0.6.1" [package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] +all = ["cryptography (>=2.1.1)", "python3-saml (>=1.5.0)"] +allpy3 = ["cryptography (>=2.1.1)", "python3-saml (>=1.5.0)"] +azuread = ["cryptography (>=2.1.1)"] +saml = ["python3-saml (>=1.5.0)"] + +[[package]] +name = "sqlparse" +version = "0.5.3" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, + {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, +] + +[package.extras] +dev = ["build", "hatch"] +doc = ["sphinx"] [[package]] name = "tomlkit" @@ -1969,6 +2456,37 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "uvicorn" version = "0.32.0" @@ -2072,83 +2590,83 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "watchfiles" -version = "1.0.0" +version = "1.0.4" description = "Simple, modern and high performance file watching and code reload in python." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "watchfiles-1.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1d19df28f99d6a81730658fbeb3ade8565ff687f95acb59665f11502b441be5f"}, - {file = "watchfiles-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28babb38cf2da8e170b706c4b84aa7e4528a6fa4f3ee55d7a0866456a1662041"}, - {file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12ab123135b2f42517f04e720526d41448667ae8249e651385afb5cda31fedc0"}, - {file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13a4f9ee0cd25682679eea5c14fc629e2eaa79aab74d963bc4e21f43b8ea1877"}, - {file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e1d9284cc84de7855fcf83472e51d32daf6f6cecd094160192628bc3fee1b78"}, - {file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ee5edc939f53466b329bbf2e58333a5461e6c7b50c980fa6117439e2c18b42d"}, - {file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dccfc70480087567720e4e36ec381bba1ed68d7e5f368fe40c93b3b1eba0105"}, - {file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83a6d33a9eda0af6a7470240d1af487807adc269704fe76a4972dd982d16236"}, - {file = "watchfiles-1.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:905f69aad276639eff3893759a07d44ea99560e67a1cf46ff389cd62f88872a2"}, - {file = "watchfiles-1.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09551237645d6bff3972592f2aa5424df9290e7a2e15d63c5f47c48cde585935"}, - {file = "watchfiles-1.0.0-cp310-none-win32.whl", hash = "sha256:d2b39aa8edd9e5f56f99a2a2740a251dc58515398e9ed5a4b3e5ff2827060755"}, - {file = "watchfiles-1.0.0-cp310-none-win_amd64.whl", hash = "sha256:2de52b499e1ab037f1a87cb8ebcb04a819bf087b1015a4cf6dcf8af3c2a2613e"}, - {file = "watchfiles-1.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fbd0ab7a9943bbddb87cbc2bf2f09317e74c77dc55b1f5657f81d04666c25269"}, - {file = "watchfiles-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:774ef36b16b7198669ce655d4f75b4c3d370e7f1cbdfb997fb10ee98717e2058"}, - {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b4fb98100267e6a5ebaff6aaa5d20aea20240584647470be39fe4823012ac96"}, - {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0fc3bf0effa2d8075b70badfdd7fb839d7aa9cea650d17886982840d71fdeabf"}, - {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:648e2b6db53eca6ef31245805cd528a16f56fa4cc15aeec97795eaf713c11435"}, - {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa13d604fcb9417ae5f2e3de676e66aa97427d888e83662ad205bed35a313176"}, - {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:936f362e7ff28311b16f0b97ec51e8f2cc451763a3264640c6ed40fb252d1ee4"}, - {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245fab124b9faf58430da547512d91734858df13f2ddd48ecfa5e493455ffccb"}, - {file = "watchfiles-1.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4ff9c7e84e8b644a8f985c42bcc81457240316f900fc72769aaedec9d088055a"}, - {file = "watchfiles-1.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c9a8d8fd97defe935ef8dd53d562e68942ad65067cd1c54d6ed8a088b1d931d"}, - {file = "watchfiles-1.0.0-cp311-none-win32.whl", hash = "sha256:a0abf173975eb9dd17bb14c191ee79999e650997cc644562f91df06060610e62"}, - {file = "watchfiles-1.0.0-cp311-none-win_amd64.whl", hash = "sha256:2a825ba4b32c214e3855b536eb1a1f7b006511d8e64b8215aac06eb680642d84"}, - {file = "watchfiles-1.0.0-cp311-none-win_arm64.whl", hash = "sha256:a5a7a06cfc65e34fd0a765a7623c5ba14707a0870703888e51d3d67107589817"}, - {file = "watchfiles-1.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:28fb64b5843d94e2c2483f7b024a1280662a44409bedee8f2f51439767e2d107"}, - {file = "watchfiles-1.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e3750434c83b61abb3163b49c64b04180b85b4dabb29a294513faec57f2ffdb7"}, - {file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bedf84835069f51c7b026b3ca04e2e747ea8ed0a77c72006172c72d28c9f69fc"}, - {file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90004553be36427c3d06ec75b804233f8f816374165d5225b93abd94ba6e7234"}, - {file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b46e15c34d4e401e976d6949ad3a74d244600d5c4b88c827a3fdf18691a46359"}, - {file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:487d15927f1b0bd24e7df921913399bb1ab94424c386bea8b267754d698f8f0e"}, - {file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ff236d7a3f4b0a42f699a22fc374ba526bc55048a70cbb299661158e1bb5e1f"}, - {file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c01446626574561756067f00b37e6b09c8622b0fc1e9fdbc7cbcea328d4e514"}, - {file = "watchfiles-1.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b551c465a59596f3d08170bd7e1c532c7260dd90ed8135778038e13c5d48aa81"}, - {file = "watchfiles-1.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1ed613ee107269f66c2df631ec0fc8efddacface85314d392a4131abe299f00"}, - {file = "watchfiles-1.0.0-cp312-none-win32.whl", hash = "sha256:5f75cd42e7e2254117cf37ff0e68c5b3f36c14543756b2da621408349bd9ca7c"}, - {file = "watchfiles-1.0.0-cp312-none-win_amd64.whl", hash = "sha256:cf517701a4a872417f4e02a136e929537743461f9ec6cdb8184d9a04f4843545"}, - {file = "watchfiles-1.0.0-cp312-none-win_arm64.whl", hash = "sha256:8a2127cd68950787ee36753e6d401c8ea368f73beaeb8e54df5516a06d1ecd82"}, - {file = "watchfiles-1.0.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:95de85c254f7fe8cbdf104731f7f87f7f73ae229493bebca3722583160e6b152"}, - {file = "watchfiles-1.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:533a7cbfe700e09780bb31c06189e39c65f06c7f447326fee707fd02f9a6e945"}, - {file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2218e78e2c6c07b1634a550095ac2a429026b2d5cbcd49a594f893f2bb8c936"}, - {file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9122b8fdadc5b341315d255ab51d04893f417df4e6c1743b0aac8bf34e96e025"}, - {file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9272fdbc0e9870dac3b505bce1466d386b4d8d6d2bacf405e603108d50446940"}, - {file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a3b33c3aefe9067ebd87846806cd5fc0b017ab70d628aaff077ab9abf4d06b3"}, - {file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc338ce9f8846543d428260fa0f9a716626963148edc937d71055d01d81e1525"}, - {file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ac778a460ea22d63c7e6fb0bc0f5b16780ff0b128f7f06e57aaec63bd339285"}, - {file = "watchfiles-1.0.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:53ae447f06f8f29f5ab40140f19abdab822387a7c426a369eb42184b021e97eb"}, - {file = "watchfiles-1.0.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1f73c2147a453315d672c1ad907abe6d40324e34a185b51e15624bc793f93cc6"}, - {file = "watchfiles-1.0.0-cp313-none-win32.whl", hash = "sha256:eba98901a2eab909dbd79681190b9049acc650f6111fde1845484a4450761e98"}, - {file = "watchfiles-1.0.0-cp313-none-win_amd64.whl", hash = "sha256:d562a6114ddafb09c33246c6ace7effa71ca4b6a2324a47f4b09b6445ea78941"}, - {file = "watchfiles-1.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3d94fd83ed54266d789f287472269c0def9120a2022674990bd24ad989ebd7a0"}, - {file = "watchfiles-1.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48051d1c504448b2fcda71c5e6e3610ae45de6a0b8f5a43b961f250be4bdf5a8"}, - {file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29cf884ad4285d23453c702ed03d689f9c0e865e3c85d20846d800d4787de00f"}, - {file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d3572d4c34c4e9c33d25b3da47d9570d5122f8433b9ac6519dca49c2740d23cd"}, - {file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c2696611182c85eb0e755b62b456f48debff484b7306b56f05478b843ca8ece"}, - {file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:550109001920a993a4383b57229c717fa73627d2a4e8fcb7ed33c7f1cddb0c85"}, - {file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b555a93c15bd2c71081922be746291d776d47521a00703163e5fbe6d2a402399"}, - {file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:947ccba18a38b85c366dafeac8df2f6176342d5992ca240a9d62588b214d731f"}, - {file = "watchfiles-1.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ffd98a299b0a74d1b704ef0ed959efb753e656a4e0425c14e46ae4c3cbdd2919"}, - {file = "watchfiles-1.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f8c4f3a1210ed099a99e6a710df4ff2f8069411059ffe30fa5f9467ebed1256b"}, - {file = "watchfiles-1.0.0-cp39-none-win32.whl", hash = "sha256:1e176b6b4119b3f369b2b4e003d53a226295ee862c0962e3afd5a1c15680b4e3"}, - {file = "watchfiles-1.0.0-cp39-none-win_amd64.whl", hash = "sha256:2d9c0518fabf4a3f373b0a94bb9e4ea7a1df18dec45e26a4d182aa8918dee855"}, - {file = "watchfiles-1.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f159ac795785cde4899e0afa539f4c723fb5dd336ce5605bc909d34edd00b79b"}, - {file = "watchfiles-1.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c3d258d78341d5d54c0c804a5b7faa66cd30ba50b2756a7161db07ce15363b8d"}, - {file = "watchfiles-1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bbd0311588c2de7f9ea5cf3922ccacfd0ec0c1922870a2be503cc7df1ca8be7"}, - {file = "watchfiles-1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a13ac46b545a7d0d50f7641eefe47d1597e7d1783a5d89e09d080e6dff44b0"}, - {file = "watchfiles-1.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2bca898c1dc073912d3db7fa6926cc08be9575add9e84872de2c99c688bac4e"}, - {file = "watchfiles-1.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:06d828fe2adc4ac8a64b875ca908b892a3603d596d43e18f7948f3fef5fc671c"}, - {file = "watchfiles-1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:074c7618cd6c807dc4eaa0982b4a9d3f8051cd0b72793511848fd64630174b17"}, - {file = "watchfiles-1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95dc785bc284552d044e561b8f4fe26d01ab5ca40d35852a6572d542adfeb4bc"}, - {file = "watchfiles-1.0.0.tar.gz", hash = "sha256:37566c844c9ce3b5deb964fe1a23378e575e74b114618d211fbda8f59d7b5dab"}, + {file = "watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08"}, + {file = "watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899"}, + {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff"}, + {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f"}, + {file = "watchfiles-1.0.4-cp310-cp310-win32.whl", hash = "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f"}, + {file = "watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161"}, + {file = "watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19"}, + {file = "watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c"}, + {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1"}, + {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226"}, + {file = "watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105"}, + {file = "watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74"}, + {file = "watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3"}, + {file = "watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2"}, + {file = "watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a"}, + {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff"}, + {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e"}, + {file = "watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94"}, + {file = "watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c"}, + {file = "watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90"}, + {file = "watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9"}, + {file = "watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902"}, + {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1"}, + {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303"}, + {file = "watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80"}, + {file = "watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc"}, + {file = "watchfiles-1.0.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d3452c1ec703aa1c61e15dfe9d482543e4145e7c45a6b8566978fbb044265a21"}, + {file = "watchfiles-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b75fee5a16826cf5c46fe1c63116e4a156924d668c38b013e6276f2582230f0"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e997802d78cdb02623b5941830ab06f8860038faf344f0d288d325cc9c5d2ff"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0611d244ce94d83f5b9aff441ad196c6e21b55f77f3c47608dcf651efe54c4a"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9745a4210b59e218ce64c91deb599ae8775c8a9da4e95fb2ee6fe745fc87d01a"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4810ea2ae622add560f4aa50c92fef975e475f7ac4900ce5ff5547b2434642d8"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d103cd01458f22462dedeb5a3382b7f2c57d07ff033fbc9465919e5e1d0f3"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbd912a61543a36aef85e34f212e5d2486e7c53ebfdb70d1e0b060cc50dd0bf"}, + {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0bc80d91ddaf95f70258cf78c471246846c1986bcc5fd33ccc4a1a67fcb40f9a"}, + {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab0311bb2ffcd9f74b6c9de2dda1612c13c84b996d032cd74799adb656af4e8b"}, + {file = "watchfiles-1.0.4-cp39-cp39-win32.whl", hash = "sha256:02a526ee5b5a09e8168314c905fc545c9bc46509896ed282aeb5a8ba9bd6ca27"}, + {file = "watchfiles-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:a5ae5706058b27c74bac987d615105da17724172d5aaacc6c362a40599b6de43"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9fe37a2de80aa785d340f2980276b17ef697ab8db6019b07ee4fd28a8359d2f3"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9d1ef56b56ed7e8f312c934436dea93bfa3e7368adfcf3df4c0da6d4de959a1e"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b42cac65beae3a362629950c444077d1b44f1790ea2772beaea95451c086bb"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e0227b8ed9074c6172cf55d85b5670199c99ab11fd27d2c473aa30aec67ee42"}, + {file = "watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205"}, ] [package.dependencies] @@ -2168,81 +2686,81 @@ files = [ [[package]] name = "websockets" -version = "14.1" +version = "14.2" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "websockets-14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a0adf84bc2e7c86e8a202537b4fd50e6f7f0e4a6b6bf64d7ccb96c4cd3330b29"}, - {file = "websockets-14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90b5d9dfbb6d07a84ed3e696012610b6da074d97453bd01e0e30744b472c8179"}, - {file = "websockets-14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2177ee3901075167f01c5e335a6685e71b162a54a89a56001f1c3e9e3d2ad250"}, - {file = "websockets-14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f14a96a0034a27f9d47fd9788913924c89612225878f8078bb9d55f859272b0"}, - {file = "websockets-14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f874ba705deea77bcf64a9da42c1f5fc2466d8f14daf410bc7d4ceae0a9fcb0"}, - {file = "websockets-14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199"}, - {file = "websockets-14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bea45f19b7ca000380fbd4e02552be86343080120d074b87f25593ce1700ad58"}, - {file = "websockets-14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:219c8187b3ceeadbf2afcf0f25a4918d02da7b944d703b97d12fb01510869078"}, - {file = "websockets-14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad2ab2547761d79926effe63de21479dfaf29834c50f98c4bf5b5480b5838434"}, - {file = "websockets-14.1-cp310-cp310-win32.whl", hash = "sha256:1288369a6a84e81b90da5dbed48610cd7e5d60af62df9851ed1d1d23a9069f10"}, - {file = "websockets-14.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0744623852f1497d825a49a99bfbec9bea4f3f946df6eb9d8a2f0c37a2fec2e"}, - {file = "websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512"}, - {file = "websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac"}, - {file = "websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280"}, - {file = "websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1"}, - {file = "websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3"}, - {file = "websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6"}, - {file = "websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0"}, - {file = "websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89"}, - {file = "websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23"}, - {file = "websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e"}, - {file = "websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09"}, - {file = "websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed"}, - {file = "websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d"}, - {file = "websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707"}, - {file = "websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a"}, - {file = "websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45"}, - {file = "websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58"}, - {file = "websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058"}, - {file = "websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4"}, - {file = "websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05"}, - {file = "websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0"}, - {file = "websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f"}, - {file = "websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9"}, - {file = "websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b"}, - {file = "websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3"}, - {file = "websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59"}, - {file = "websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2"}, - {file = "websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da"}, - {file = "websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9"}, - {file = "websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7"}, - {file = "websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a"}, - {file = "websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6"}, - {file = "websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0"}, - {file = "websockets-14.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:01bb2d4f0a6d04538d3c5dfd27c0643269656c28045a53439cbf1c004f90897a"}, - {file = "websockets-14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:414ffe86f4d6f434a8c3b7913655a1a5383b617f9bf38720e7c0799fac3ab1c6"}, - {file = "websockets-14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fda642151d5affdee8a430bd85496f2e2517be3a2b9d2484d633d5712b15c56"}, - {file = "websockets-14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd7c11968bc3860d5c78577f0dbc535257ccec41750675d58d8dc66aa47fe52c"}, - {file = "websockets-14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a032855dc7db987dff813583d04f4950d14326665d7e714d584560b140ae6b8b"}, - {file = "websockets-14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e7ea2f782408c32d86b87a0d2c1fd8871b0399dd762364c731d86c86069a78"}, - {file = "websockets-14.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:39450e6215f7d9f6f7bc2a6da21d79374729f5d052333da4d5825af8a97e6735"}, - {file = "websockets-14.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ceada5be22fa5a5a4cdeec74e761c2ee7db287208f54c718f2df4b7e200b8d4a"}, - {file = "websockets-14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3fc753451d471cff90b8f467a1fc0ae64031cf2d81b7b34e1811b7e2691bc4bc"}, - {file = "websockets-14.1-cp39-cp39-win32.whl", hash = "sha256:14839f54786987ccd9d03ed7f334baec0f02272e7ec4f6e9d427ff584aeea8b4"}, - {file = "websockets-14.1-cp39-cp39-win_amd64.whl", hash = "sha256:d9fd19ecc3a4d5ae82ddbfb30962cf6d874ff943e56e0c81f5169be2fda62979"}, - {file = "websockets-14.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5dc25a9dbd1a7f61eca4b7cb04e74ae4b963d658f9e4f9aad9cd00b688692c8"}, - {file = "websockets-14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:04a97aca96ca2acedf0d1f332c861c5a4486fdcba7bcef35873820f940c4231e"}, - {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df174ece723b228d3e8734a6f2a6febbd413ddec39b3dc592f5a4aa0aff28098"}, - {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:034feb9f4286476f273b9a245fb15f02c34d9586a5bc936aff108c3ba1b21beb"}, - {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c308dabd2b380807ab64b62985eaccf923a78ebc572bd485375b9ca2b7dc7"}, - {file = "websockets-14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a42d3ecbb2db5080fc578314439b1d79eef71d323dc661aa616fb492436af5d"}, - {file = "websockets-14.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddaa4a390af911da6f680be8be4ff5aaf31c4c834c1a9147bc21cbcbca2d4370"}, - {file = "websockets-14.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a4c805c6034206143fbabd2d259ec5e757f8b29d0a2f0bf3d2fe5d1f60147a4a"}, - {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:205f672a6c2c671a86d33f6d47c9b35781a998728d2c7c2a3e1cf3333fcb62b7"}, - {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef440054124728cc49b01c33469de06755e5a7a4e83ef61934ad95fc327fbb0"}, - {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7591d6f440af7f73c4bd9404f3772bfee064e639d2b6cc8c94076e71b2471c1"}, - {file = "websockets-14.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:25225cc79cfebc95ba1d24cd3ab86aaa35bcd315d12fa4358939bd55e9bd74a5"}, - {file = "websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e"}, - {file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"}, + {file = "websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885"}, + {file = "websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397"}, + {file = "websockets-14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d"}, + {file = "websockets-14.2-cp310-cp310-win32.whl", hash = "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d"}, + {file = "websockets-14.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2"}, + {file = "websockets-14.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166"}, + {file = "websockets-14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f"}, + {file = "websockets-14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d"}, + {file = "websockets-14.2-cp311-cp311-win32.whl", hash = "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a"}, + {file = "websockets-14.2-cp311-cp311-win_amd64.whl", hash = "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b"}, + {file = "websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c"}, + {file = "websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967"}, + {file = "websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe"}, + {file = "websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205"}, + {file = "websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce"}, + {file = "websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e"}, + {file = "websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad"}, + {file = "websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307"}, + {file = "websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc"}, + {file = "websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f"}, + {file = "websockets-14.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7cd5706caec1686c5d233bc76243ff64b1c0dc445339bd538f30547e787c11fe"}, + {file = "websockets-14.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec607328ce95a2f12b595f7ae4c5d71bf502212bddcea528290b35c286932b12"}, + {file = "websockets-14.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da85651270c6bfb630136423037dd4975199e5d4114cae6d3066641adcc9d1c7"}, + {file = "websockets-14.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ecadc7ce90accf39903815697917643f5b7cfb73c96702318a096c00aa71f5"}, + {file = "websockets-14.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1979bee04af6a78608024bad6dfcc0cc930ce819f9e10342a29a05b5320355d0"}, + {file = "websockets-14.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dddacad58e2614a24938a50b85969d56f88e620e3f897b7d80ac0d8a5800258"}, + {file = "websockets-14.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:89a71173caaf75fa71a09a5f614f450ba3ec84ad9fca47cb2422a860676716f0"}, + {file = "websockets-14.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6af6a4b26eea4fc06c6818a6b962a952441e0e39548b44773502761ded8cc1d4"}, + {file = "websockets-14.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:80c8efa38957f20bba0117b48737993643204645e9ec45512579132508477cfc"}, + {file = "websockets-14.2-cp39-cp39-win32.whl", hash = "sha256:2e20c5f517e2163d76e2729104abc42639c41cf91f7b1839295be43302713661"}, + {file = "websockets-14.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4c8cef610e8d7c70dea92e62b6814a8cd24fbd01d7103cc89308d2bfe1659ef"}, + {file = "websockets-14.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29"}, + {file = "websockets-14.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a"}, + {file = "websockets-14.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3"}, + {file = "websockets-14.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bbe03eb853e17fd5b15448328b4ec7fb2407d45fb0245036d06a3af251f8e48f"}, + {file = "websockets-14.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3c4aa3428b904d5404a0ed85f3644d37e2cb25996b7f096d77caeb0e96a3b42"}, + {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577a4cebf1ceaf0b65ffc42c54856214165fb8ceeba3935852fc33f6b0c55e7f"}, + {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad1c1d02357b7665e700eca43a31d52814ad9ad9b89b58118bdabc365454b574"}, + {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f390024a47d904613577df83ba700bd189eedc09c57af0a904e5c39624621270"}, + {file = "websockets-14.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3c1426c021c38cf92b453cdf371228d3430acd775edee6bac5a4d577efc72365"}, + {file = "websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b"}, + {file = "websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5"}, ] [[package]] @@ -2343,9 +2861,9 @@ multidict = ">=4.0" propcache = ">=0.2.0" [extras] -dev = ["black", "flake8-pyproject", "isort", "pre-commit", "pyright"] +dev = ["black", "django-debug-toolbar", "flake8-pyproject", "isort", "pre-commit", "pyinstrument", "pyright"] [metadata] lock-version = "2.1" python-versions = ">=3.12, <3.14" -content-hash = "9d619a424e335e94cc4590f4a3bc0aa036771f8823c49e709605c263145c2261" +content-hash = "99aa857190d75e7e57ca382147704e51ef7c10bbc2dfd68329e17a7a928791e6" diff --git a/pyproject.toml b/pyproject.toml index 8cd7df36..ec58cc29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,15 +16,22 @@ dependencies = [ "discord.py==2.4.0", "audioop-lts==0.2.1 ; python_version >= '3.13'", - # fastapi - "fastapi==0.115.4", - "fastapi-admin @ git+https://github.com/Ballsdex-Team/fastapi-admin@ballsdex", - "uvicorn==0.32.0", - # database ORM "tortoise-orm[asyncpg] ==0.22.2", "tortoise-cli==0.1.2", + # django admin panel + "uvicorn[standard]==0.32.0", + "django==5.1.4", + "django-nonrelated-inlines==0.2", + "django-admin-autocomplete-filter==0.7.1", + "django-admin-action-forms==1.3.0", + "django-admin-inline-paginator==0.4.0", + "dj-database-url==2.3.0", + "social-auth-app-django==5.4.2", + "psycopg==3.2.3", + "psycopg-binary==3.2.3", + # metrics "prometheus-client==0.20.0", @@ -42,8 +49,10 @@ dev = [ "pre-commit==3.7.1", "black==24.8.0", "flake8-pyproject==1.2.3", - "pyright==1.1.335", + "pyright==1.1.390", "isort==5.13.2", + "django-debug-toolbar==4.4.6", + "pyinstrument==5.0.0", ] [tool.poetry] @@ -75,9 +84,16 @@ src_folder = "./ballsdex" line-length = 99 [tool.flake8] -ignore = "W503,E203" +ignore = "W503,E203,E999" max-line-length = 99 +exclude = "./admin_panel/admin_panel/settings/*" [tool.isort] profile = "black" line_length = 99 + +[tool.pyright] +extraPaths = ["./admin_panel"] +pythonVersion = "3.13" +reportIncompatibleMethodOverride = "warning" +reportIncompatibleVariableOverride = "warning"