Compare commits

..

No commits in common. "f557fa462af0a95015fbee9c21e1e5e1f3f3128a" and "9bf600845af6f7aa0a96c45fd128be4d61f35264" have entirely different histories.

36 changed files with 163 additions and 744 deletions

View file

@ -1,7 +1,6 @@
.PHONY: help install run restart test test-xml test-html uninstall backup \
generate-env export-env make-messages compile-messages \
format check typecheck precommit clear make-migrations migrate \
delete-elasticsearch
format check typecheck precommit clear make-migrations migrate
# Detect OS and set script paths
ifeq ($(OS),Windows_NT)
@ -29,25 +28,24 @@ help: clear
@echo "Usage: make [target]"
@echo ""
@echo "Targets:"
@echo " install Pull and build Docker images"
@echo " run Start all services"
@echo " restart Restart all services"
@echo " test Run tests with coverage"
@echo " test-xml Generate XML coverage report"
@echo " test-html Generate HTML coverage report"
@echo " uninstall Remove containers, volumes, and generated files"
@echo " delete-elasticsearch Wipe Elasticsearch data (recreated on restart)"
@echo " backup Create a backup"
@echo " generate-env Generate .env file from template"
@echo " export-env Export environment variables"
@echo " make-messages Extract translation strings"
@echo " compile-messages Compile translation files"
@echo " make-migrations Generate migration files"
@echo " migrate Apply migration files"
@echo " format Format code with ruff"
@echo " check Lint code with ruff"
@echo " typecheck Typecheck code with ty"
@echo " precommit Run format, check, and typecheck"
@echo " install Pull and build Docker images"
@echo " run Start all services"
@echo " restart Restart all services"
@echo " test Run tests with coverage"
@echo " test-xml Generate XML coverage report"
@echo " test-html Generate HTML coverage report"
@echo " uninstall Remove containers, volumes, and generated files"
@echo " backup Create a backup"
@echo " generate-env Generate .env file from template"
@echo " export-env Export environment variables"
@echo " make-messages Extract translation strings"
@echo " compile-messages Compile translation files"
@echo " make-migrations Generate migration files"
@echo " migrate Apply migration files"
@echo " format Format code with ruff"
@echo " check Lint code with ruff"
@echo " typecheck Typecheck code with ty"
@echo " precommit Run format, check, and typecheck"
@echo ""
@echo "Detected OS: $(if $(filter Windows_NT,$(OS)),Windows,Unix)"
@echo "Scripts directory: $(SCRIPT_DIR)"
@ -108,6 +106,3 @@ migrate: clear
@$(call RUN_SCRIPT,migrate)
migration: clear make-migrations migrate
delete-elasticsearch: clear
@$(call RUN_SCRIPT,delete-elasticsearch)

View file

@ -1,7 +0,0 @@
services:
database:
ports:
- "5432:5432"
elasticsearch:
ports:
- "9200:9200"

View file

@ -34,6 +34,8 @@ services:
restart: always
volumes:
- postgres-data:/var/lib/postgresql/data/
ports:
- "5432:5432"
env_file:
- .env
logging: *default-logging
@ -90,22 +92,19 @@ services:
elasticsearch:
container_name: elasticsearch
image: git.wiseless.xyz/fureunoir/schon-elasticsearch:9.3.1
image: wiseless/elasticsearch-maxed:8.16.6
restart: always
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms512m -Xmx512m
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=false
- xpack.security.transport.ssl.enabled=false
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
- xpack.security.enabled=false
env_file:
- .env
volumes:
- es-data:/usr/share/elasticsearch/data
logging: *default-logging
healthcheck:
test: [ "CMD", "curl", "-f", "-u", "elastic:${ELASTIC_PASSWORD}", "http://localhost:9200/_cluster/health" ]
test: [ "CMD", "curl", "-f", "http://localhost:9200" ]
interval: 10s
timeout: 5s
retries: 5
@ -119,6 +118,8 @@ services:
- .env
command:
- "--es.uri=http://elastic:${ELASTIC_PASSWORD}@elasticsearch:9200"
ports:
- "9114:9114"
depends_on:
elasticsearch:
condition: service_healthy
@ -212,12 +213,12 @@ services:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./monitoring/web.yml:/etc/prometheus/web.yml:ro
- prometheus-data:/prometheus
ports:
- "9090:9090"
command:
- --config.file=/etc/prometheus/prometheus.yml
- --web.config.file=/etc/prometheus/web.yml
logging: *default-logging
ports:
- "9090:9090"
storefront:
container_name: storefront

View file

@ -7,7 +7,6 @@ from django.core.exceptions import BadRequest
from django.db.models import (
Avg,
Case,
DecimalField,
Exists,
FloatField,
IntegerField,
@ -179,7 +178,7 @@ class ProductFilter(FilterSet):
price_order=Coalesce(
Max("stocks__price"),
Value(0.0),
output_field=DecimalField(max_digits=12, decimal_places=2),
output_field=FloatField(),
)
)

View file

@ -25,7 +25,6 @@ from engine.core.utils.emailing import contact_us_email
from engine.core.utils.messages import permission_denied_message
from engine.core.utils.nominatim import fetch_address_suggestions
from engine.payments.graphene.object_types import TransactionType
from schon.utils.ratelimit import graphql_ratelimit
from schon.utils.renderers import camelize
logger = logging.getLogger(__name__)
@ -203,7 +202,6 @@ class BuyOrder(Mutation):
transaction = Field(TransactionType, required=False)
@staticmethod
@graphql_ratelimit(rate="10/h")
def mutate(
parent,
info,
@ -314,7 +312,7 @@ class BulkWishlistAction(Mutation):
description = _("perform an action on a list of products in the wishlist")
class Arguments:
wishlist_uuid = UUID(required=True)
wishlist_uuid = UUID(required=False)
action = String(required=True, description="remove/add")
products = List(BulkProductInput, required=True)
@ -326,8 +324,10 @@ class BulkWishlistAction(Mutation):
info,
action,
products,
wishlist_uuid,
wishlist_uuid=None,
):
if not wishlist_uuid:
raise BadRequest(_("please provide wishlist_uuid value"))
user = info.context.user
try:
wishlist = Wishlist.objects.get(user=user, uuid=wishlist_uuid)
@ -366,7 +366,6 @@ class BuyUnregisteredOrder(Mutation):
transaction = Field(TransactionType, required=False)
@staticmethod
@graphql_ratelimit(rate="10/h")
def mutate(
parent,
info,
@ -494,7 +493,6 @@ class BuyWishlist(Mutation):
transaction = Field(TransactionType, required=False)
@staticmethod
@graphql_ratelimit(rate="10/h")
def mutate(parent, info, wishlist_uuid, force_balance=False, force_payment=False):
user = info.context.user
try:
@ -551,7 +549,6 @@ class BuyProduct(Mutation):
transaction = Field(TransactionType, required=False)
@staticmethod
@graphql_ratelimit(rate="10/h")
def mutate(
parent,
info,
@ -591,7 +588,6 @@ class FeedbackProductAction(Mutation):
feedback = Field(FeedbackType, required=False)
@staticmethod
@graphql_ratelimit(rate="10/h")
def mutate(parent, info, order_product_uuid, action, comment=None, rating=None):
user = info.context.user
try:
@ -691,7 +687,6 @@ class ContactUs(Mutation):
error = String()
@staticmethod
@graphql_ratelimit(rate="2/h")
def mutate(parent, info, email, name, subject, message, phone_number=None):
try:
contact_us_email.delay(

View file

@ -494,7 +494,7 @@ class OrderType(DjangoObjectType):
description = _("orders")
def resolve_total_price(self: Order, _info) -> float:
return self.total_price # ty: ignore[invalid-return-type]
return self.total_price
def resolve_total_quantity(self: Order, _info) -> int:
return self.total_quantity

View file

@ -1,46 +0,0 @@
# Generated by Django 5.2.11 on 2026-03-02 21:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0058_product_video_alter_address_api_response_and_more"),
]
operations = [
migrations.AlterField(
model_name="orderproduct",
name="buy_price",
field=models.DecimalField(
blank=True,
decimal_places=2,
help_text="the price paid by the customer for this product at purchase time",
max_digits=12,
null=True,
verbose_name="purchase price at order time",
),
),
migrations.AlterField(
model_name="stock",
name="price",
field=models.DecimalField(
decimal_places=2,
default=0,
help_text="final price to the customer after markups",
max_digits=12,
verbose_name="selling price",
),
),
migrations.AlterField(
model_name="stock",
name="purchase_price",
field=models.DecimalField(
decimal_places=2,
default=0,
help_text="the price paid to the vendor for this product",
max_digits=12,
verbose_name="vendor purchase price",
),
),
]

View file

@ -2,7 +2,6 @@ import datetime
import json
import logging
from contextlib import suppress
from decimal import Decimal
from typing import TYPE_CHECKING, Any, Iterable, Self
from constance import config
@ -587,10 +586,8 @@ class Stock(NiceModel):
help_text=_("the vendor supplying this product stock"),
verbose_name=_("associated vendor"),
)
price = DecimalField(
max_digits=12,
decimal_places=2,
default=0,
price = FloatField(
default=0.0,
help_text=_("final price to the customer after markups"),
verbose_name=_("selling price"),
)
@ -603,10 +600,8 @@ class Stock(NiceModel):
blank=True,
null=True,
)
purchase_price = DecimalField(
max_digits=12,
decimal_places=2,
default=0,
purchase_price = FloatField(
default=0.0,
help_text=_("the price paid to the vendor for this product"),
verbose_name=_("vendor purchase price"),
)
@ -1477,15 +1472,12 @@ class Order(NiceModel):
)
@property
def total_price(self) -> Decimal:
def total_price(self) -> float:
total = self.order_products.exclude(status__in=FAILED_STATUSES).aggregate(
total=Sum(
F("buy_price") * F("quantity"),
output_field=DecimalField(max_digits=12, decimal_places=2),
)
total=Sum(F("buy_price") * F("quantity"), output_field=FloatField())
)["total"]
return (total or Decimal("0.00")).quantize(Decimal("0.01"))
return round(total or 0.0, 2)
@property
def total_quantity(self) -> int:
@ -1979,9 +1971,7 @@ class OrderProduct(NiceModel):
is_publicly_visible = False
buy_price = DecimalField(
max_digits=12,
decimal_places=2,
buy_price = FloatField(
blank=True,
null=True,
help_text=_("the price paid by the customer for this product at purchase time"),

View file

@ -435,4 +435,4 @@ class OrderDetailSerializer(ModelSerializer):
]
def get_total_price(self, obj: Order) -> float:
return obj.total_price # ty: ignore[invalid-return-type]
return obj.total_price

View file

@ -307,4 +307,4 @@ class OrderSimpleSerializer(ModelSerializer):
]
def get_total_price(self, obj: Order) -> float:
return obj.total_price # ty: ignore[invalid-return-type]
return obj.total_price

View file

@ -125,7 +125,7 @@ sitemap_detail.__doc__ = _( # ty:ignore[invalid-assignment]
_graphql_validation_rules = [QueryDepthLimitRule]
if getenv("GRAPHQL_INTROSPECTION", "").lower() not in ("1", "true", "yes"):
if getenv("GRAPHQL_INTROSPECTION", "").lower() in ("1", "true", "yes"):
_graphql_validation_rules.append(NoSchemaIntrospectionCustomRule)

View file

@ -5,7 +5,6 @@ from rest_framework.exceptions import PermissionDenied
from engine.core.utils.messages import permission_denied_message
from engine.payments.graphene.object_types import TransactionType
from engine.payments.models import Transaction
from schon.utils.ratelimit import graphql_ratelimit
class Deposit(Mutation):
@ -14,7 +13,6 @@ class Deposit(Mutation):
transaction = graphene.Field(TransactionType)
@graphql_ratelimit(rate="10/h")
def mutate(self, info, amount):
if info.context.user.is_authenticated:
transaction = Transaction.objects.create(

View file

@ -1,7 +1,6 @@
from django.db.models import (
BooleanField,
Case,
DecimalField,
F,
Manager,
Q,
@ -23,16 +22,14 @@ class GatewayQuerySet(QuerySet):
Sum(
"transactions__amount", filter=Q(transactions__created__date=today)
),
Value(0),
output_field=DecimalField(max_digits=12, decimal_places=2),
Value(0.0),
),
monthly_sum=Coalesce(
Sum(
"transactions__amount",
filter=Q(transactions__created__date__gte=current_month_start),
),
Value(0),
output_field=DecimalField(max_digits=12, decimal_places=2),
Value(0.0),
),
)

View file

@ -1,42 +0,0 @@
# Generated by Django 5.2.11 on 2026-03-02 21:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("payments", "0006_transaction_payments_tr_created_95e595_idx"),
]
operations = [
migrations.AlterField(
model_name="balance",
name="amount",
field=models.DecimalField(decimal_places=2, default=0, max_digits=12),
),
migrations.AlterField(
model_name="gateway",
name="maximum_transaction_amount",
field=models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
verbose_name="maximum transaction amount",
),
),
migrations.AlterField(
model_name="gateway",
name="minimum_transaction_amount",
field=models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
verbose_name="minimum transaction amount",
),
),
migrations.AlterField(
model_name="transaction",
name="amount",
field=models.DecimalField(decimal_places=2, max_digits=12),
),
]

View file

@ -5,7 +5,7 @@ from django.contrib.postgres.indexes import GinIndex
from django.db.models import (
CASCADE,
CharField,
DecimalField,
FloatField,
ForeignKey,
Index,
JSONField,
@ -24,7 +24,7 @@ from schon.utils.misc import create_object
class Transaction(NiceModel):
amount = DecimalField(max_digits=12, decimal_places=2, null=False, blank=False)
amount = FloatField(null=False, blank=False)
balance = ForeignKey(
"payments.Balance",
on_delete=CASCADE,
@ -59,6 +59,8 @@ class Transaction(NiceModel):
)
def save(self, **kwargs): # ty: ignore[invalid-method-override]
if len(str(self.amount).split(".")[1]) > 2:
self.amount = round(self.amount, 2)
super().save(**kwargs)
return self
@ -72,9 +74,7 @@ class Transaction(NiceModel):
class Balance(NiceModel):
amount = DecimalField(
max_digits=12, decimal_places=2, null=False, blank=False, default=0
)
amount = FloatField(null=False, blank=False, default=0)
user = OneToOneField(
to=settings.AUTH_USER_MODEL,
on_delete=CASCADE,
@ -92,6 +92,8 @@ class Balance(NiceModel):
verbose_name_plural = _("balances")
def save(self, **kwargs): # ty: ignore[invalid-method-override]
if self.amount != 0.0 and len(str(self.amount).split(".")[1]) > 2:
self.amount = round(self.amount, 2)
super().save(**kwargs)
@ -116,21 +118,11 @@ class Gateway(NiceModel):
),
)
integration_path = CharField(max_length=255, null=True, blank=True)
minimum_transaction_amount = DecimalField(
max_digits=12,
decimal_places=2,
null=False,
blank=False,
default=0,
verbose_name=_("minimum transaction amount"),
minimum_transaction_amount = FloatField(
null=False, blank=False, default=0, verbose_name=_("minimum transaction amount")
)
maximum_transaction_amount = DecimalField(
max_digits=12,
decimal_places=2,
null=False,
blank=False,
default=0,
verbose_name=_("maximum transaction amount"),
maximum_transaction_amount = FloatField(
null=False, blank=False, default=0, verbose_name=_("maximum transaction amount")
)
daily_limit = PositiveIntegerField(
null=False,

View file

@ -1,19 +1,11 @@
from decimal import Decimal
from rest_framework.fields import DecimalField, JSONField, SerializerMethodField
from rest_framework.fields import FloatField, JSONField, SerializerMethodField
from rest_framework.serializers import ModelSerializer, Serializer
from engine.payments.models import Transaction
class DepositSerializer(Serializer):
amount = DecimalField(
max_digits=12,
decimal_places=2,
required=True,
min_value=Decimal("0.01"),
max_value=Decimal("999999.99"),
)
amount = FloatField(required=True)
class TransactionSerializer(ModelSerializer):
@ -40,5 +32,5 @@ class TransactionProcessSerializer(ModelSerializer):
class LimitsSerializer(Serializer):
min_amount = DecimalField(max_digits=12, decimal_places=2, read_only=True)
max_amount = DecimalField(max_digits=12, decimal_places=2, read_only=True)
min_amount = FloatField(read_only=True)
max_amount = FloatField(read_only=True)

View file

@ -46,7 +46,6 @@ from engine.vibes_auth.emailing.tasks import (
)
from engine.vibes_auth.forms import UserForm
from engine.vibes_auth.models import (
AdminOTPCode,
BlacklistedToken,
ChatMessage,
ChatThread,
@ -404,17 +403,6 @@ class OutstandingTokenAdmin(BaseOutstandingTokenAdmin, ModelAdmin):
pass
@register(AdminOTPCode)
class AdminOTPCodeAdmin(ModelAdmin):
list_display = ("user", "code", "is_used", "created")
list_filter = ("is_used",)
search_fields = ("user__email",)
readonly_fields = ("user", "code", "is_used", "created", "modified")
def has_add_permission(self, request):
return False
admin.site.register(User, UserAdmin)
admin.site.unregister(BaseGroup)

View file

@ -1,102 +0,0 @@
import logging
from django import forms
from django.contrib import messages
from django.contrib.auth import authenticate, login
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import render
from django.urls import path, reverse
from django.utils.translation import gettext_lazy as _
from unfold.sites import UnfoldAdminSite
from engine.vibes_auth.utils.otp import generate_otp_code, send_admin_otp_email_task
logger = logging.getLogger(__name__)
class OTPVerifyForm(forms.Form):
code = forms.CharField(
max_length=6,
min_length=6,
label=_("Verification code"),
widget=forms.TextInput(
attrs={
"autofocus": True,
"autocomplete": "one-time-code",
"inputmode": "numeric",
"pattern": "[0-9]*",
}
),
)
class SchonAdminSite(UnfoldAdminSite):
def login(self, request: HttpRequest, extra_context=None) -> HttpResponse:
if request.method == "POST":
email = request.POST.get("username", "")
password = request.POST.get("password", "")
user = authenticate(request, username=email, password=password)
if user is not None and user.is_staff: # ty: ignore[unresolved-attribute]
code = generate_otp_code(user)
send_admin_otp_email_task.delay(user_pk=str(user.pk), code=code)
request.session["_2fa_user_id"] = str(user.pk)
messages.info(
request, _("A verification code has been sent to your email.")
)
return HttpResponseRedirect(reverse("admin:verify-otp"))
return super().login(request, extra_context)
def extra_urls(self):
return [
path(
"verify-otp/",
self.verify_otp_view,
name="verify-otp",
),
]
def verify_otp_view(self, request: HttpRequest) -> HttpResponse:
from engine.vibes_auth.models import AdminOTPCode, User
user_pk = request.session.get("_2fa_user_id")
if not user_pk:
return HttpResponseRedirect(reverse("admin:login"))
try:
user = User.objects.get(pk=user_pk)
except User.DoesNotExist:
return HttpResponseRedirect(reverse("admin:login"))
form = OTPVerifyForm()
error = None
if request.method == "POST":
form = OTPVerifyForm(request.POST)
if form.is_valid():
code = form.cleaned_data["code"]
otp = (
AdminOTPCode.objects.filter(user=user, code=code, is_used=False)
.order_by("-created")
.first()
)
if otp and otp.is_valid():
otp.is_used = True
otp.save(update_fields=["is_used", "modified"])
del request.session["_2fa_user_id"]
login(request, user)
return HttpResponseRedirect(reverse("admin:index"))
else:
error = _("Invalid or expired code. Please try again.")
context = {
**self.each_context(request),
"form": form,
"error": error,
"title": _("Two-factor authentication"),
"site_title": self.site_title,
"site_header": self.site_header,
}
return render(request, "admin/verify_otp.html", context)

View file

@ -11,12 +11,4 @@ class VibesAuthConfig(AppConfig):
hide = False
def ready(self) -> None:
from django.contrib import admin # noqa: E402
from django.contrib.admin import sites # noqa: E402
import engine.vibes_auth.signals # noqa: F401
from engine.vibes_auth.admin_site import SchonAdminSite # noqa: E402
site = SchonAdminSite()
admin.site = site
sites.site = site

View file

@ -26,7 +26,6 @@ from engine.vibes_auth.serializers import (
)
from engine.vibes_auth.utils.emailing import send_reset_password_email_task
from engine.vibes_auth.validators import is_valid_email, is_valid_phone_number
from schon.utils.ratelimit import graphql_ratelimit
logger = logging.getLogger(__name__)
@ -50,7 +49,6 @@ class CreateUser(Mutation):
success = Boolean()
@graphql_ratelimit(rate="5/h")
def mutate(
self,
info,
@ -222,7 +220,6 @@ class ObtainJSONWebToken(Mutation):
refresh_token = String(required=True)
access_token = String(required=True)
@graphql_ratelimit(rate="10/h")
def mutate(self, info, email, password):
serializer = TokenObtainPairSerializer(
data={"email": email, "password": password}, retrieve_user=False
@ -247,7 +244,6 @@ class RefreshJSONWebToken(Mutation):
user = Field(UserType)
refresh_token = String()
@graphql_ratelimit(rate="10/h")
def mutate(self, info, refresh_token):
serializer = TokenRefreshSerializer(
data={"refresh": refresh_token}, retrieve_user=False
@ -298,7 +294,6 @@ class ActivateUser(Mutation):
success = Boolean()
@graphql_ratelimit(rate="5/h")
def mutate(self, info, uid, token):
try:
token = urlsafe_base64_decode(token).decode()
@ -328,7 +323,6 @@ class ResetPassword(Mutation):
success = Boolean()
@graphql_ratelimit(rate="4/h")
def mutate(self, info, email):
try:
user = User.objects.get(email=email)
@ -351,7 +345,6 @@ class ConfirmResetPassword(Mutation):
success = Boolean()
@graphql_ratelimit(rate="5/h")
def mutate(self, info, uid, token, password, confirm_password):
try:
if not compare_digest(password, confirm_password):
@ -383,28 +376,16 @@ class ConfirmResetPassword(Mutation):
raise BadRequest(_(f"something went wrong: {e!s}")) from e
ALLOWED_IMAGE_EXTENSIONS = {"jpg", "jpeg", "png", "gif", "webp"}
MAX_AVATAR_SIZE = 5 * 1024 * 1024 # 5 MB
class UploadAvatar(Mutation):
class Arguments:
file = Upload(required=True)
user = Field(UserType)
@graphql_ratelimit(rate="3/h")
def mutate(self, info, file):
if not info.context.user.is_authenticated:
raise PermissionDenied(permission_denied_message)
ext = file.name.rsplit(".", 1)[-1].lower() if "." in file.name else ""
if ext not in ALLOWED_IMAGE_EXTENSIONS:
raise BadRequest(_("only image files are allowed (jpg, png, gif, webp)"))
if file.size > MAX_AVATAR_SIZE:
raise BadRequest(_("file size must not exceed 5 MB"))
try:
info.context.user.avatar = file
info.context.user.save()

View file

@ -1,26 +0,0 @@
from django.core.management.base import BaseCommand, CommandError
from engine.vibes_auth.models import User
class Command(BaseCommand):
help = "Directly activate a user account (for when SMTP is not configured)"
def add_arguments(self, parser):
parser.add_argument("email", type=str, help="Email of the user to activate")
def handle(self, *args, **options):
email = options["email"]
try:
user = User.objects.get(email=email)
except User.DoesNotExist as e:
raise CommandError(f'User "{email}" does not exist.') from e
if user.is_active and user.is_verified:
self.stdout.write(f'User "{email}" is already active and verified.')
return
user.is_active = True
user.is_verified = True
user.save(update_fields=["is_active", "is_verified", "modified"])
self.stdout.write(self.style.SUCCESS(f'User "{email}" has been activated.'))

View file

@ -1,25 +0,0 @@
from django.core.management.base import BaseCommand, CommandError
from engine.vibes_auth.models import User
from engine.vibes_auth.utils.otp import generate_otp_code
class Command(BaseCommand):
help = "Generate a fresh admin OTP code for a user (for when SMTP is down)"
def add_arguments(self, parser):
parser.add_argument("email", type=str, help="Email of the staff user")
def handle(self, *args, **options):
email = options["email"]
try:
user = User.objects.get(email=email)
except User.DoesNotExist as e:
raise CommandError(f'User "{email}" does not exist.') from e
if not user.is_staff:
raise CommandError(f'User "{email}" is not a staff member.')
code = generate_otp_code(user)
self.stdout.write(f"OTP code for {email}: {code}")
self.stdout.write("Valid for 5 minutes.")

View file

@ -1,78 +0,0 @@
# Generated by Django 5.2.11 on 2026-03-02 21:39
import uuid
import django.db.models.deletion
import django_extensions.db.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("vibes_auth", "0011_alter_user_attributes_alter_user_phone_number"),
]
operations = [
migrations.AlterField(
model_name="user",
name="activation_token",
field=models.CharField(
blank=True, default="", max_length=128, verbose_name="activation token"
),
),
migrations.CreateModel(
name="AdminOTPCode",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="unique id is used to surely identify any database object",
primary_key=True,
serialize=False,
verbose_name="unique id",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="if set to false, this object can't be seen by users without needed permission",
verbose_name="is active",
),
),
(
"created",
django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True,
help_text="when the object first appeared on the database",
verbose_name="created",
),
),
(
"modified",
django_extensions.db.fields.ModificationDateTimeField(
auto_now=True,
help_text="when the object was last modified",
verbose_name="modified",
),
),
("code", models.CharField(max_length=6)),
("is_used", models.BooleanField(default=False)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="otp_codes",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "admin OTP code",
"verbose_name_plural": "admin OTP codes",
},
),
]

View file

@ -94,9 +94,7 @@ class User(AbstractUser, NiceModel):
default=False,
)
activation_token = CharField(
max_length=128, blank=True, default="", verbose_name=_("activation token")
)
activation_token = UUIDField(default=uuid4, verbose_name=_("activation token"))
activation_token_created = DateTimeField(
null=True,
blank=True,
@ -150,25 +148,17 @@ class User(AbstractUser, NiceModel):
def save(self, *args, **kwargs):
if self._state.adding and self.activation_token_created is None:
self.activation_token_created = timezone.now()
if self._state.adding and not self.activation_token:
self.refresh_activation_token()
super().save(*args, **kwargs)
def refresh_activation_token(self) -> str:
"""Generate a fresh activation token, store its hash, return raw token."""
import hashlib
raw_token = str(uuid4())
self.activation_token = hashlib.sha256(raw_token.encode()).hexdigest()
def refresh_activation_token(self) -> None:
"""Generate a fresh activation token and update its timestamp."""
self.activation_token = uuid4()
self.activation_token_created = timezone.now()
return raw_token
def check_token(self, token) -> bool:
import hashlib
from datetime import timedelta
hashed = hashlib.sha256(str(token).encode()).hexdigest()
if hashed != self.activation_token:
if str(token) != str(self.activation_token):
return False
if self.activation_token_created:
if timezone.now() > self.activation_token_created + timedelta(hours=24):
@ -184,25 +174,6 @@ class User(AbstractUser, NiceModel):
verbose_name_plural = _("users")
class AdminOTPCode(NiceModel):
user = ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=CASCADE,
related_name="otp_codes",
)
code = CharField(max_length=6)
is_used = BooleanField(default=False)
def is_valid(self) -> bool:
from datetime import timedelta
return not self.is_used and timezone.now() < self.created + timedelta(minutes=5)
class Meta:
verbose_name = _("admin OTP code")
verbose_name_plural = _("admin OTP codes")
class ChatThread(NiceModel):
user = ForeignKey(
User, null=True, blank=True, on_delete=SET_NULL, related_name="chat_threads"

View file

@ -1,31 +0,0 @@
{% extends 'unfold/layouts/unauthenticated.html' %}
{% load i18n unfold %}
{% block title %}
{{ title }} | {{ site_title }}
{% endblock %}
{% block content %}
{% include "unfold/helpers/unauthenticated_title.html" with title=site_title subtitle=_('Two-factor authentication') %}
{% include "unfold/helpers/messages.html" %}
{% if error %}
<div class="flex flex-col gap-4 mb-8 *:mb-0">
{% include "unfold/helpers/messages/error.html" with errors=error %}
</div>
{% endif %}
<form method="post">
{% csrf_token %}
{% include "unfold/helpers/field.html" with field=form.code %}
<div class="flex flex-col gap-3 mt-6">
{% component "unfold/components/button.html" with submit=1 variant="primary" class="w-full" %}
{% translate 'Verify' %} <span class="material-symbols-outlined relative right-0 transition-all group-hover:-right-1 text-sm">check</span>
{% endcomponent %}
</div>
</form>
{% endblock %}

View file

@ -19,9 +19,6 @@ def send_verification_email_task(user_pk: str) -> tuple[bool, str]:
user = User.objects.get(pk=user_pk)
user.refresh_from_db()
raw_token = user.refresh_activation_token()
user.save(update_fields=["activation_token", "activation_token_created"])
activate(user.language)
email_subject = _(f"{settings.PROJECT_NAME} | Activate Account")
@ -30,7 +27,7 @@ def send_verification_email_task(user_pk: str) -> tuple[bool, str]:
{
"user_first_name": user.first_name,
"activation_link": f"https://{settings.STOREFRONT_DOMAIN}/{user.language}/activate-user?uid={urlsafe_base64_encode(force_bytes(user.uuid))}"
f"&token={urlsafe_base64_encode(force_bytes(raw_token))}",
f"&token={urlsafe_base64_encode(force_bytes(user.activation_token))}",
"project_name": settings.PROJECT_NAME,
},
)

View file

@ -1,36 +0,0 @@
import secrets
from celery.app import shared_task
from constance import config
from django.conf import settings
from django.core.mail import EmailMessage
from engine.core.utils import get_dynamic_email_connection
def generate_otp_code(user) -> str:
from engine.vibes_auth.models import AdminOTPCode
AdminOTPCode.objects.filter(user=user, is_used=False).update(is_used=True)
code = f"{secrets.randbelow(1000000):06d}"
AdminOTPCode.objects.create(user=user, code=code)
return code
@shared_task(queue="default")
def send_admin_otp_email_task(user_pk: str, code: str) -> tuple[bool, str]:
from engine.vibes_auth.models import User
try:
user = User.objects.get(pk=user_pk)
email = EmailMessage(
subject=f"{settings.PROJECT_NAME} | Admin Login Code",
body=f"Your admin login code: {code}\n\nValid for 5 minutes.",
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
to=[user.email],
connection=get_dynamic_email_connection(),
)
email.send()
except Exception as e:
return False, str(e)
return True, str(user.uuid)

View file

@ -139,6 +139,8 @@ class UserViewSet(
pending = User.objects.get(
email=email, is_active=False, is_verified=False
)
pending.refresh_activation_token()
pending.save()
send_verification_email_task.delay(user_pk=str(pending.uuid))
return Response(
{

View file

@ -19,7 +19,7 @@ dependencies = [
"django-constance==4.3.4",
"django-cors-headers==4.9.0",
"django-dbbackup==5.2.0",
"django-elasticsearch-dsl==9.0",
"django-elasticsearch-dsl==8.2",
"django-extensions==4.1",
"django-fernet-encrypted-fields==0.3.1",
"django-filter==25.2",
@ -47,6 +47,7 @@ dependencies = [
"drf-spectacular==0.29.0",
"drf-spectacular-websocket==1.3.1",
"drf-orjson-renderer==1.8.0",
"elasticsearch-dsl==8.18.0",
"filelock==3.25.0",
"filetype==1.2.0",
"graphene-django==3.2.3",
@ -78,7 +79,7 @@ dependencies = [
[project.optional-dependencies]
worker = [
"celery==5.6.2",
"django-celery-beat==2.9.0",
"django-celery-beat==2.8.1",
"django-celery-results==2.6.0",
]
linting = [

View file

@ -5,9 +5,9 @@ from schon.settings.base import DEBUG
ELASTICSEARCH_DSL = {
"default": {
"hosts": ["http://elasticsearch:9200"],
"basic_auth": ("elastic", getenv("ELASTIC_PASSWORD")),
"http_auth": ("elastic", getenv("ELASTIC_PASSWORD")),
"verify_certs": False,
"request_timeout": 30,
"timeout": 30,
"ssl_show_warn": False,
"max_retries": 3,
"retry_on_timeout": True,

View file

@ -1,28 +0,0 @@
from functools import wraps
from django.conf import settings
from django_ratelimit.core import is_ratelimited
from schon.utils.misc import RatelimitedError
def graphql_ratelimit(key="ip", rate="10/h", group=None):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
info = args[1] if len(args) > 1 else kwargs.get("info")
request = info.context # ty: ignore[possibly-missing-attribute]
actual_rate = rate if not settings.DEBUG else "888/h"
if is_ratelimited(
request=request,
group=group or f"graphql:{func.__qualname__}",
key=key,
rate=actual_rate,
increment=True,
):
raise RatelimitedError()
return func(*args, **kwargs)
return wrapper
return decorator

View file

@ -1,24 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
source ./scripts/Unix/starter.sh
# Stop the elasticsearch container
log_step "Stopping Elasticsearch container..."
if ! docker compose stop elasticsearch; then
log_warning "Elasticsearch container may not be running"
fi
log_success "Elasticsearch container stopped!"
# Remove the elasticsearch container
log_step "Removing Elasticsearch container..."
docker compose rm -f elasticsearch || log_warning "Failed to remove Elasticsearch container"
log_success "Elasticsearch container removed!"
# Remove the es-data volume
log_step "Removing Elasticsearch data volume..."
docker volume rm -f schon_es-data || log_warning "Failed to remove es-data volume (may not exist)"
log_success "Elasticsearch data volume removed!"
echo
log_result "Elasticsearch has been wiped. Run 'make restart' to recreate and reindex."

View file

@ -1,38 +0,0 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Load shared utilities
$utilsPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Definition) '..\lib\utils.ps1'
. $utilsPath
.\scripts\Windows\starter.ps1
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
# Stop the elasticsearch container
Write-Step "Stopping Elasticsearch container..."
docker compose stop elasticsearch
if ($LASTEXITCODE -ne 0) {
Write-Warning-Custom "Elasticsearch container may not be running"
}
Write-Success "Elasticsearch container stopped!"
# Remove the elasticsearch container
Write-Step "Removing Elasticsearch container..."
docker compose rm -f elasticsearch
if ($LASTEXITCODE -ne 0) {
Write-Warning-Custom "Failed to remove Elasticsearch container"
}
Write-Success "Elasticsearch container removed!"
# Remove the es-data volume
Write-Step "Removing Elasticsearch data volume..."
docker volume rm -f schon_es-data
if ($LASTEXITCODE -ne 0) {
Write-Warning-Custom "Failed to remove es-data volume (may not exist)"
}
Write-Success "Elasticsearch data volume removed!"
Write-Result ""
Write-Result "Elasticsearch has been wiped. Run 'make restart' to recreate and reindex."

165
uv.lock
View file

@ -634,11 +634,14 @@ wheels = [
[[package]]
name = "cron-descriptor"
version = "1.4.5"
version = "2.0.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/02/83/70bd410dc6965e33a5460b7da84cf0c5a7330a68d6d5d4c3dfdb72ca117e/cron_descriptor-1.4.5.tar.gz", hash = "sha256:f51ce4ffc1d1f2816939add8524f206c376a42c87a5fca3091ce26725b3b1bca", size = 30666, upload-time = "2024-08-24T18:16:48.654Z" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7c/31/0b21d1599656b2ffa6043e51ca01041cd1c0f6dacf5a3e2b620ed120e7d8/cron_descriptor-2.0.6.tar.gz", hash = "sha256:e39d2848e1d8913cfb6e3452e701b5eec662ee18bea8cc5aa53ee1a7bb217157", size = 49456, upload-time = "2025-09-03T16:30:22.434Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/20/2cfe598ead23a715a00beb716477cfddd3e5948cf203c372d02221e5b0c6/cron_descriptor-1.4.5-py3-none-any.whl", hash = "sha256:736b3ae9d1a99bc3dbfc5b55b5e6e7c12031e7ba5de716625772f8b02dcd6013", size = 50370, upload-time = "2024-08-24T18:16:46.783Z" },
{ url = "https://files.pythonhosted.org/packages/21/cc/361326a54ad92e2e12845ad15e335a4e14b8953665007fb514d3393dfb0f/cron_descriptor-2.0.6-py3-none-any.whl", hash = "sha256:3a1c0d837c0e5a32e415f821b36cf758eb92d510e6beff8fbfe4fa16573d93d6", size = 74446, upload-time = "2025-09-03T16:30:21.397Z" },
]
[[package]]
@ -763,7 +766,7 @@ wheels = [
[[package]]
name = "django-celery-beat"
version = "2.9.0"
version = "2.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "celery" },
@ -773,9 +776,9 @@ dependencies = [
{ name = "python-crontab" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/05/45/fc97bc1d9af8e7dc07f1e37044d9551a30e6793249864cef802341e2e3a8/django_celery_beat-2.9.0.tar.gz", hash = "sha256:92404650f52fcb44cf08e2b09635cb1558327c54b1a5d570f0e2d3a22130934c", size = 177667, upload-time = "2026-02-28T16:45:34.749Z" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/11/0c8b412869b4fda72828572068312b10aafe7ccef7b41af3633af31f9d4b/django_celery_beat-2.8.1.tar.gz", hash = "sha256:dfad0201c0ac50c91a34700ef8fa0a10ee098cc7f3375fe5debed79f2204f80a", size = 175802, upload-time = "2025-05-13T06:58:29.246Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/ae/9befa7ae37f5e5c41be636a254fcf47ff30dd5c88bd115070e252f6b9162/django_celery_beat-2.9.0-py3-none-any.whl", hash = "sha256:4a9e5ebe26d6f8d7215e1fc5c46e466016279dc102435a28141108649bdf2157", size = 105013, upload-time = "2026-02-28T16:45:32.822Z" },
{ url = "https://files.pythonhosted.org/packages/61/e5/3a0167044773dee989b498e9a851fc1663bea9ab879f1179f7b8a827ac10/django_celery_beat-2.8.1-py3-none-any.whl", hash = "sha256:da2b1c6939495c05a551717509d6e3b79444e114a027f7b77bf3727c2a39d171", size = 104833, upload-time = "2025-05-13T06:58:27.309Z" },
]
[[package]]
@ -840,15 +843,15 @@ wheels = [
[[package]]
name = "django-elasticsearch-dsl"
version = "9.0"
version = "8.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "elasticsearch" },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/92/3ea6a083afaccf758d41ff470bc4ff3bfbbfa411ce243816400d48ae42de/django_elasticsearch_dsl-9.0.tar.gz", hash = "sha256:2fb39478fcde20a3ab1b800676e57ef3de15327c0eda16d192aca3823651bdaf", size = 31579, upload-time = "2025-07-23T22:49:39.751Z" }
sdist = { url = "https://files.pythonhosted.org/packages/91/d6/5a79e4061ca0551a2b100ed66a2f0a897eb2ee1ad555b18bf7bfb4f4e2ed/django_elasticsearch_dsl-8.2.tar.gz", hash = "sha256:6ecbe9688db7b0314f12067b781aa02750c849438b3ddd513a01054c56e21661", size = 31480, upload-time = "2025-06-23T22:31:21.763Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/ca/67bc5ca78cdfbb53bd0d8d6cdafef17231241a85c62cc56ab6dae9a9a815/django_elasticsearch_dsl-9.0-py2.py3-none-any.whl", hash = "sha256:728c691d9d2cf413e902444e180cfcccc79adbbe9b59eb82860ea73ae7ef3a9f", size = 20976, upload-time = "2025-07-23T22:49:34.704Z" },
{ url = "https://files.pythonhosted.org/packages/75/50/43a0b89152ff5f4f136becd8102e073805661a8d76f59f569d806c2dc799/django_elasticsearch_dsl-8.2-py2.py3-none-any.whl", hash = "sha256:c674881f9f14ed7566a9eb384237c06812bffaf3c7a4ef387b8d7a66bb107689", size = 20940, upload-time = "2025-06-23T22:31:19.352Z" },
]
[[package]]
@ -1274,32 +1277,44 @@ wheels = [
[[package]]
name = "elastic-transport"
version = "9.2.1"
version = "8.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "sniffio" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/23/0a/a92140b666afdcb9862a16e4d80873b3c887c1b7e3f17e945fc3460edf1b/elastic_transport-9.2.1.tar.gz", hash = "sha256:97d9abd638ba8aa90faa4ca1bf1a18bde0fe2088fbc8757f2eb7b299f205773d", size = 77403, upload-time = "2025-12-23T11:54:12.849Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6a/54/d498a766ac8fa475f931da85a154666cc81a70f8eb4a780bc8e4e934e9ac/elastic_transport-8.17.1.tar.gz", hash = "sha256:5edef32ac864dca8e2f0a613ef63491ee8d6b8cfb52881fa7313ba9290cac6d2", size = 73425, upload-time = "2025-03-13T07:28:30.776Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e6/a42b600ae8b808371f740381f6c32050cad93f870d36cc697b8b7006bf7c/elastic_transport-9.2.1-py3-none-any.whl", hash = "sha256:39e1a25e486af34ce7aa1bc9005d1c736f1b6fb04c9b64ea0604ded5a61fc1d4", size = 65327, upload-time = "2025-12-23T11:54:11.681Z" },
{ url = "https://files.pythonhosted.org/packages/cf/cd/b71d5bc74cde7fc6fd9b2ff9389890f45d9762cbbbf81dc5e51fd7588c4a/elastic_transport-8.17.1-py3-none-any.whl", hash = "sha256:192718f498f1d10c5e9aa8b9cf32aed405e469a7f0e9d6a8923431dbb2c59fb8", size = 64969, upload-time = "2025-03-13T07:28:29.031Z" },
]
[[package]]
name = "elasticsearch"
version = "9.3.0"
version = "8.19.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "elastic-transport" },
{ name = "python-dateutil" },
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0d/15/283459c9299d412ffa2aaab69b082857631c519233f5491d6c567e3320ca/elasticsearch-9.3.0.tar.gz", hash = "sha256:f76e149c0a22d5ccbba58bdc30c9f51cf894231b359ef4fd7e839b558b59f856", size = 893538, upload-time = "2026-02-03T20:26:38.914Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6b/79/365e306017a9fcfbbefab1a3b588d2404bea8806b36766ff0f886509a20e/elasticsearch-8.19.3.tar.gz", hash = "sha256:e84dd618a220cac25b962790085045dd27ac72e01c0a5d81bd29a2d47a71f03f", size = 800298, upload-time = "2025-12-23T12:56:00.72Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/37/3a196f8918743f2104cb66b1f56218079ecac6e128c061de7df7f4faef02/elasticsearch-9.3.0-py3-none-any.whl", hash = "sha256:67bd2bb4f0800f58c2847d29cd57d6e7bf5bc273483b4f17421f93e75ba09f39", size = 979405, upload-time = "2026-02-03T20:26:34.552Z" },
{ url = "https://files.pythonhosted.org/packages/56/0f/ac126833c385b06166d41c486e4911f58ad7791fd1a53dd6e0b8d16ff214/elasticsearch-8.19.3-py3-none-any.whl", hash = "sha256:fe1db2555811192e8a1be78b01234d0a49d32b185ea7eeeb6f059331dee32838", size = 952820, upload-time = "2025-12-23T12:55:56.796Z" },
]
[[package]]
name = "elasticsearch-dsl"
version = "8.18.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "elastic-transport" },
{ name = "elasticsearch" },
{ name = "python-dateutil" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/6d/00cbeee412a2dc825f0df18c98463a2e0b423b86800fba6c50ea2c627962/elasticsearch_dsl-8.18.0.tar.gz", hash = "sha256:763465dba9eae166add10567e924c65730aa122819b08bfe9a077e91b13b30d1", size = 31886, upload-time = "2025-04-16T11:54:14.412Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/a9/b200790a22585aeb023d88bd8b9fb222820e2976ce4239d401670116ae3c/elasticsearch_dsl-8.18.0-py3-none-any.whl", hash = "sha256:0522c5bb20c7abae69855109e650bf1166d486cbf706b5e1b29c28936a9102a3", size = 10406, upload-time = "2025-04-16T11:54:12.677Z" },
]
[[package]]
@ -3375,6 +3390,7 @@ dependencies = [
{ name = "drf-orjson-renderer" },
{ name = "drf-spectacular" },
{ name = "drf-spectacular-websocket" },
{ name = "elasticsearch-dsl" },
{ name = "filelock" },
{ name = "filetype" },
{ name = "graphene-django" },
@ -3444,13 +3460,13 @@ requires-dist = [
{ name = "cryptography", specifier = "==46.0.5" },
{ name = "django", specifier = "==5.2.11" },
{ name = "django-cacheops", specifier = "==7.2" },
{ name = "django-celery-beat", marker = "extra == 'worker'", specifier = "==2.9.0" },
{ name = "django-celery-beat", marker = "extra == 'worker'", specifier = "==2.8.1" },
{ name = "django-celery-results", marker = "extra == 'worker'", specifier = "==2.6.0" },
{ name = "django-constance", specifier = "==4.3.4" },
{ name = "django-cors-headers", specifier = "==4.9.0" },
{ name = "django-dbbackup", specifier = "==5.2.0" },
{ name = "django-debug-toolbar", specifier = "==6.2.0" },
{ name = "django-elasticsearch-dsl", specifier = "==9.0" },
{ name = "django-elasticsearch-dsl", specifier = "==8.2" },
{ name = "django-extensions", specifier = "==4.1" },
{ name = "django-fernet-encrypted-fields", specifier = "==0.3.1" },
{ name = "django-filter", specifier = "==25.2" },
@ -3479,6 +3495,7 @@ requires-dist = [
{ name = "drf-orjson-renderer", specifier = "==1.8.0" },
{ name = "drf-spectacular", specifier = "==0.29.0" },
{ name = "drf-spectacular-websocket", specifier = "==1.3.1" },
{ name = "elasticsearch-dsl", specifier = "==8.18.0" },
{ name = "filelock", specifier = "==3.25.0" },
{ name = "filetype", specifier = "==1.2.0" },
{ name = "graphene-django", specifier = "==3.2.3" },
@ -4085,70 +4102,64 @@ wheels = [
[[package]]
name = "yarl"
version = "1.23.0"
version = "1.22.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" }
sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" },
{ url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" },
{ url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" },
{ url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" },
{ url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" },
{ url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" },
{ url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" },
{ url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" },
{ url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" },
{ url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" },
{ url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" },
{ url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" },
{ url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" },
{ url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" },
{ url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" },
{ url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" },
{ url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" },
{ url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" },
{ url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" },
{ url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" },
{ url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" },
{ url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" },
{ url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" },
{ url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" },
{ url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" },
{ url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" },
{ url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" },
{ url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" },
{ url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" },
{ url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" },
{ url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" },
{ url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" },
{ url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" },
{ url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" },
{ url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" },
{ url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" },
{ url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" },
{ url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" },
{ url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" },
{ url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" },
{ url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" },
{ url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" },
{ url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" },
{ url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" },
{ url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" },
{ url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" },
{ url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" },
{ url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" },
{ url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" },
{ url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" },
{ url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" },
{ url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" },
{ url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" },
{ url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" },
{ url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" },
{ url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" },
{ url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" },
{ url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" },
{ url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" },
{ url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" },
{ url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" },
{ url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" },
{ url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" },
{ url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" },
{ url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" },
{ url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" },
{ url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" },
{ url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" },
{ url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" },
{ url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
{ url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
{ url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },
{ url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" },
{ url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" },
{ url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" },
{ url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" },
{ url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" },
{ url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" },
{ url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" },
{ url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" },
{ url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" },
{ url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" },
{ url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" },
{ url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" },
{ url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" },
{ url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" },
{ url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" },
{ url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" },
{ url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" },
{ url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" },
{ url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" },
{ url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" },
{ url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" },
{ url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" },
{ url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" },
{ url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" },
{ url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" },
{ url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" },
{ url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" },
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
]
[[package]]