diff --git a/Makefile b/Makefile
index 12e60654..aec1a2ff 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,7 @@
.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
+ format check typecheck precommit clear make-migrations migrate \
+ delete-elasticsearch
# Detect OS and set script paths
ifeq ($(OS),Windows_NT)
@@ -28,24 +29,25 @@ 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 " 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 " 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 ""
@echo "Detected OS: $(if $(filter Windows_NT,$(OS)),Windows,Unix)"
@echo "Scripts directory: $(SCRIPT_DIR)"
@@ -106,3 +108,6 @@ migrate: clear
@$(call RUN_SCRIPT,migrate)
migration: clear make-migrations migrate
+
+delete-elasticsearch: clear
+ @$(call RUN_SCRIPT,delete-elasticsearch)
diff --git a/docker-compose.override.yml b/docker-compose.override.yml
new file mode 100644
index 00000000..07402f8a
--- /dev/null
+++ b/docker-compose.override.yml
@@ -0,0 +1,7 @@
+services:
+ database:
+ ports:
+ - "5432:5432"
+ elasticsearch:
+ ports:
+ - "9200:9200"
diff --git a/docker-compose.yml b/docker-compose.yml
index c7536c32..991e6b4d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -34,8 +34,6 @@ services:
restart: always
volumes:
- postgres-data:/var/lib/postgresql/data/
- ports:
- - "5432:5432"
env_file:
- .env
logging: *default-logging
@@ -92,19 +90,22 @@ services:
elasticsearch:
container_name: elasticsearch
- image: wiseless/elasticsearch-maxed:8.16.6
+ image: git.wiseless.xyz/fureunoir/schon-elasticsearch:9.3.1
restart: always
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms512m -Xmx512m
- - xpack.security.enabled=false
+ - xpack.security.enabled=true
+ - xpack.security.http.ssl.enabled=false
+ - xpack.security.transport.ssl.enabled=false
+ - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
env_file:
- .env
volumes:
- es-data:/usr/share/elasticsearch/data
logging: *default-logging
healthcheck:
- test: [ "CMD", "curl", "-f", "http://localhost:9200" ]
+ test: [ "CMD", "curl", "-f", "-u", "elastic:${ELASTIC_PASSWORD}", "http://localhost:9200/_cluster/health" ]
interval: 10s
timeout: 5s
retries: 5
@@ -118,8 +119,6 @@ services:
- .env
command:
- "--es.uri=http://elastic:${ELASTIC_PASSWORD}@elasticsearch:9200"
- ports:
- - "9114:9114"
depends_on:
elasticsearch:
condition: service_healthy
@@ -213,12 +212,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
diff --git a/engine/core/filters.py b/engine/core/filters.py
index 839cce17..ef1ff8ab 100644
--- a/engine/core/filters.py
+++ b/engine/core/filters.py
@@ -7,6 +7,7 @@ from django.core.exceptions import BadRequest
from django.db.models import (
Avg,
Case,
+ DecimalField,
Exists,
FloatField,
IntegerField,
@@ -178,7 +179,7 @@ class ProductFilter(FilterSet):
price_order=Coalesce(
Max("stocks__price"),
Value(0.0),
- output_field=FloatField(),
+ output_field=DecimalField(max_digits=12, decimal_places=2),
)
)
diff --git a/engine/core/graphene/mutations.py b/engine/core/graphene/mutations.py
index eca0f2c3..b01097f3 100644
--- a/engine/core/graphene/mutations.py
+++ b/engine/core/graphene/mutations.py
@@ -25,6 +25,7 @@ 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__)
@@ -202,6 +203,7 @@ class BuyOrder(Mutation):
transaction = Field(TransactionType, required=False)
@staticmethod
+ @graphql_ratelimit(rate="10/h")
def mutate(
parent,
info,
@@ -312,7 +314,7 @@ class BulkWishlistAction(Mutation):
description = _("perform an action on a list of products in the wishlist")
class Arguments:
- wishlist_uuid = UUID(required=False)
+ wishlist_uuid = UUID(required=True)
action = String(required=True, description="remove/add")
products = List(BulkProductInput, required=True)
@@ -324,10 +326,8 @@ class BulkWishlistAction(Mutation):
info,
action,
products,
- wishlist_uuid=None,
+ wishlist_uuid,
):
- 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,6 +366,7 @@ class BuyUnregisteredOrder(Mutation):
transaction = Field(TransactionType, required=False)
@staticmethod
+ @graphql_ratelimit(rate="10/h")
def mutate(
parent,
info,
@@ -493,6 +494,7 @@ 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:
@@ -549,6 +551,7 @@ class BuyProduct(Mutation):
transaction = Field(TransactionType, required=False)
@staticmethod
+ @graphql_ratelimit(rate="10/h")
def mutate(
parent,
info,
@@ -588,6 +591,7 @@ 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:
@@ -687,6 +691,7 @@ 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(
diff --git a/engine/core/graphene/object_types.py b/engine/core/graphene/object_types.py
index dd4a0b1d..d5d6f687 100644
--- a/engine/core/graphene/object_types.py
+++ b/engine/core/graphene/object_types.py
@@ -494,7 +494,7 @@ class OrderType(DjangoObjectType):
description = _("orders")
def resolve_total_price(self: Order, _info) -> float:
- return self.total_price
+ return self.total_price # ty: ignore[invalid-return-type]
def resolve_total_quantity(self: Order, _info) -> int:
return self.total_quantity
diff --git a/engine/core/migrations/0059_alter_orderproduct_buy_price_alter_stock_price_and_more.py b/engine/core/migrations/0059_alter_orderproduct_buy_price_alter_stock_price_and_more.py
new file mode 100644
index 00000000..e53090d3
--- /dev/null
+++ b/engine/core/migrations/0059_alter_orderproduct_buy_price_alter_stock_price_and_more.py
@@ -0,0 +1,46 @@
+# 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",
+ ),
+ ),
+ ]
diff --git a/engine/core/models.py b/engine/core/models.py
index ff62c4d4..e044f437 100644
--- a/engine/core/models.py
+++ b/engine/core/models.py
@@ -2,6 +2,7 @@ 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
@@ -586,8 +587,10 @@ class Stock(NiceModel):
help_text=_("the vendor supplying this product stock"),
verbose_name=_("associated vendor"),
)
- price = FloatField(
- default=0.0,
+ price = DecimalField(
+ max_digits=12,
+ decimal_places=2,
+ default=0,
help_text=_("final price to the customer after markups"),
verbose_name=_("selling price"),
)
@@ -600,8 +603,10 @@ class Stock(NiceModel):
blank=True,
null=True,
)
- purchase_price = FloatField(
- default=0.0,
+ purchase_price = DecimalField(
+ max_digits=12,
+ decimal_places=2,
+ default=0,
help_text=_("the price paid to the vendor for this product"),
verbose_name=_("vendor purchase price"),
)
@@ -1472,12 +1477,15 @@ class Order(NiceModel):
)
@property
- def total_price(self) -> float:
+ def total_price(self) -> Decimal:
total = self.order_products.exclude(status__in=FAILED_STATUSES).aggregate(
- total=Sum(F("buy_price") * F("quantity"), output_field=FloatField())
+ total=Sum(
+ F("buy_price") * F("quantity"),
+ output_field=DecimalField(max_digits=12, decimal_places=2),
+ )
)["total"]
- return round(total or 0.0, 2)
+ return (total or Decimal("0.00")).quantize(Decimal("0.01"))
@property
def total_quantity(self) -> int:
@@ -1971,7 +1979,9 @@ class OrderProduct(NiceModel):
is_publicly_visible = False
- buy_price = FloatField(
+ buy_price = DecimalField(
+ max_digits=12,
+ decimal_places=2,
blank=True,
null=True,
help_text=_("the price paid by the customer for this product at purchase time"),
diff --git a/engine/core/serializers/detail.py b/engine/core/serializers/detail.py
index cf698755..8d560611 100644
--- a/engine/core/serializers/detail.py
+++ b/engine/core/serializers/detail.py
@@ -435,4 +435,4 @@ class OrderDetailSerializer(ModelSerializer):
]
def get_total_price(self, obj: Order) -> float:
- return obj.total_price
+ return obj.total_price # ty: ignore[invalid-return-type]
diff --git a/engine/core/serializers/simple.py b/engine/core/serializers/simple.py
index 6be4064b..249d2da5 100644
--- a/engine/core/serializers/simple.py
+++ b/engine/core/serializers/simple.py
@@ -307,4 +307,4 @@ class OrderSimpleSerializer(ModelSerializer):
]
def get_total_price(self, obj: Order) -> float:
- return obj.total_price
+ return obj.total_price # ty: ignore[invalid-return-type]
diff --git a/engine/core/views.py b/engine/core/views.py
index aa13f590..43a4b447 100644
--- a/engine/core/views.py
+++ b/engine/core/views.py
@@ -125,7 +125,7 @@ sitemap_detail.__doc__ = _( # ty:ignore[invalid-assignment]
_graphql_validation_rules = [QueryDepthLimitRule]
-if getenv("GRAPHQL_INTROSPECTION", "").lower() in ("1", "true", "yes"):
+if getenv("GRAPHQL_INTROSPECTION", "").lower() not in ("1", "true", "yes"):
_graphql_validation_rules.append(NoSchemaIntrospectionCustomRule)
diff --git a/engine/payments/graphene/mutations.py b/engine/payments/graphene/mutations.py
index 720b5c66..9c382161 100644
--- a/engine/payments/graphene/mutations.py
+++ b/engine/payments/graphene/mutations.py
@@ -5,6 +5,7 @@ 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):
@@ -13,6 +14,7 @@ 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(
diff --git a/engine/payments/managers.py b/engine/payments/managers.py
index fd06c513..01fb5b05 100644
--- a/engine/payments/managers.py
+++ b/engine/payments/managers.py
@@ -1,6 +1,7 @@
from django.db.models import (
BooleanField,
Case,
+ DecimalField,
F,
Manager,
Q,
@@ -22,14 +23,16 @@ class GatewayQuerySet(QuerySet):
Sum(
"transactions__amount", filter=Q(transactions__created__date=today)
),
- Value(0.0),
+ Value(0),
+ output_field=DecimalField(max_digits=12, decimal_places=2),
),
monthly_sum=Coalesce(
Sum(
"transactions__amount",
filter=Q(transactions__created__date__gte=current_month_start),
),
- Value(0.0),
+ Value(0),
+ output_field=DecimalField(max_digits=12, decimal_places=2),
),
)
diff --git a/engine/payments/migrations/0007_alter_balance_amount_and_more.py b/engine/payments/migrations/0007_alter_balance_amount_and_more.py
new file mode 100644
index 00000000..8533837b
--- /dev/null
+++ b/engine/payments/migrations/0007_alter_balance_amount_and_more.py
@@ -0,0 +1,42 @@
+# 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),
+ ),
+ ]
diff --git a/engine/payments/models.py b/engine/payments/models.py
index 6070c97f..0812ae2c 100644
--- a/engine/payments/models.py
+++ b/engine/payments/models.py
@@ -5,7 +5,7 @@ from django.contrib.postgres.indexes import GinIndex
from django.db.models import (
CASCADE,
CharField,
- FloatField,
+ DecimalField,
ForeignKey,
Index,
JSONField,
@@ -24,7 +24,7 @@ from schon.utils.misc import create_object
class Transaction(NiceModel):
- amount = FloatField(null=False, blank=False)
+ amount = DecimalField(max_digits=12, decimal_places=2, null=False, blank=False)
balance = ForeignKey(
"payments.Balance",
on_delete=CASCADE,
@@ -59,8 +59,6 @@ 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
@@ -74,7 +72,9 @@ class Transaction(NiceModel):
class Balance(NiceModel):
- amount = FloatField(null=False, blank=False, default=0)
+ amount = DecimalField(
+ max_digits=12, decimal_places=2, null=False, blank=False, default=0
+ )
user = OneToOneField(
to=settings.AUTH_USER_MODEL,
on_delete=CASCADE,
@@ -92,8 +92,6 @@ 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)
@@ -118,11 +116,21 @@ class Gateway(NiceModel):
),
)
integration_path = CharField(max_length=255, null=True, blank=True)
- minimum_transaction_amount = FloatField(
- null=False, blank=False, default=0, verbose_name=_("minimum transaction amount")
+ minimum_transaction_amount = DecimalField(
+ max_digits=12,
+ decimal_places=2,
+ null=False,
+ blank=False,
+ default=0,
+ verbose_name=_("minimum transaction amount"),
)
- maximum_transaction_amount = FloatField(
- null=False, blank=False, default=0, verbose_name=_("maximum transaction amount")
+ maximum_transaction_amount = DecimalField(
+ max_digits=12,
+ decimal_places=2,
+ null=False,
+ blank=False,
+ default=0,
+ verbose_name=_("maximum transaction amount"),
)
daily_limit = PositiveIntegerField(
null=False,
diff --git a/engine/payments/serializers.py b/engine/payments/serializers.py
index 4c89b6f6..950098a5 100644
--- a/engine/payments/serializers.py
+++ b/engine/payments/serializers.py
@@ -1,11 +1,19 @@
-from rest_framework.fields import FloatField, JSONField, SerializerMethodField
+from decimal import Decimal
+
+from rest_framework.fields import DecimalField, JSONField, SerializerMethodField
from rest_framework.serializers import ModelSerializer, Serializer
from engine.payments.models import Transaction
class DepositSerializer(Serializer):
- amount = FloatField(required=True)
+ amount = DecimalField(
+ max_digits=12,
+ decimal_places=2,
+ required=True,
+ min_value=Decimal("0.01"),
+ max_value=Decimal("999999.99"),
+ )
class TransactionSerializer(ModelSerializer):
@@ -32,5 +40,5 @@ class TransactionProcessSerializer(ModelSerializer):
class LimitsSerializer(Serializer):
- min_amount = FloatField(read_only=True)
- max_amount = FloatField(read_only=True)
+ min_amount = DecimalField(max_digits=12, decimal_places=2, read_only=True)
+ max_amount = DecimalField(max_digits=12, decimal_places=2, read_only=True)
diff --git a/engine/vibes_auth/admin.py b/engine/vibes_auth/admin.py
index b6d66805..e4aee031 100644
--- a/engine/vibes_auth/admin.py
+++ b/engine/vibes_auth/admin.py
@@ -46,6 +46,7 @@ from engine.vibes_auth.emailing.tasks import (
)
from engine.vibes_auth.forms import UserForm
from engine.vibes_auth.models import (
+ AdminOTPCode,
BlacklistedToken,
ChatMessage,
ChatThread,
@@ -403,6 +404,17 @@ 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)
diff --git a/engine/vibes_auth/admin_site.py b/engine/vibes_auth/admin_site.py
new file mode 100644
index 00000000..4a8b75ac
--- /dev/null
+++ b/engine/vibes_auth/admin_site.py
@@ -0,0 +1,102 @@
+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)
diff --git a/engine/vibes_auth/apps.py b/engine/vibes_auth/apps.py
index 82f6b125..bb611749 100644
--- a/engine/vibes_auth/apps.py
+++ b/engine/vibes_auth/apps.py
@@ -11,4 +11,12 @@ 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
diff --git a/engine/vibes_auth/graphene/mutations.py b/engine/vibes_auth/graphene/mutations.py
index b04fea2a..4563b5c9 100644
--- a/engine/vibes_auth/graphene/mutations.py
+++ b/engine/vibes_auth/graphene/mutations.py
@@ -26,6 +26,7 @@ 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__)
@@ -49,6 +50,7 @@ class CreateUser(Mutation):
success = Boolean()
+ @graphql_ratelimit(rate="5/h")
def mutate(
self,
info,
@@ -220,6 +222,7 @@ 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
@@ -244,6 +247,7 @@ 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
@@ -294,6 +298,7 @@ class ActivateUser(Mutation):
success = Boolean()
+ @graphql_ratelimit(rate="5/h")
def mutate(self, info, uid, token):
try:
token = urlsafe_base64_decode(token).decode()
@@ -323,6 +328,7 @@ class ResetPassword(Mutation):
success = Boolean()
+ @graphql_ratelimit(rate="4/h")
def mutate(self, info, email):
try:
user = User.objects.get(email=email)
@@ -345,6 +351,7 @@ 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):
@@ -376,16 +383,28 @@ 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()
diff --git a/engine/vibes_auth/management/__init__.py b/engine/vibes_auth/management/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/engine/vibes_auth/management/commands/__init__.py b/engine/vibes_auth/management/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/engine/vibes_auth/management/commands/activate_user.py b/engine/vibes_auth/management/commands/activate_user.py
new file mode 100644
index 00000000..e8f917ed
--- /dev/null
+++ b/engine/vibes_auth/management/commands/activate_user.py
@@ -0,0 +1,26 @@
+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.'))
diff --git a/engine/vibes_auth/management/commands/get_otp.py b/engine/vibes_auth/management/commands/get_otp.py
new file mode 100644
index 00000000..3c936123
--- /dev/null
+++ b/engine/vibes_auth/management/commands/get_otp.py
@@ -0,0 +1,25 @@
+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.")
diff --git a/engine/vibes_auth/migrations/0012_alter_user_activation_token_adminotpcode.py b/engine/vibes_auth/migrations/0012_alter_user_activation_token_adminotpcode.py
new file mode 100644
index 00000000..3c119622
--- /dev/null
+++ b/engine/vibes_auth/migrations/0012_alter_user_activation_token_adminotpcode.py
@@ -0,0 +1,78 @@
+# 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",
+ },
+ ),
+ ]
diff --git a/engine/vibes_auth/models.py b/engine/vibes_auth/models.py
index a55e9f46..5de10b49 100644
--- a/engine/vibes_auth/models.py
+++ b/engine/vibes_auth/models.py
@@ -94,7 +94,9 @@ class User(AbstractUser, NiceModel):
default=False,
)
- activation_token = UUIDField(default=uuid4, verbose_name=_("activation token"))
+ activation_token = CharField(
+ max_length=128, blank=True, default="", verbose_name=_("activation token")
+ )
activation_token_created = DateTimeField(
null=True,
blank=True,
@@ -148,17 +150,25 @@ 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) -> None:
- """Generate a fresh activation token and update its timestamp."""
- self.activation_token = uuid4()
+ 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()
self.activation_token_created = timezone.now()
+ return raw_token
def check_token(self, token) -> bool:
+ import hashlib
from datetime import timedelta
- if str(token) != str(self.activation_token):
+ hashed = hashlib.sha256(str(token).encode()).hexdigest()
+ if hashed != self.activation_token:
return False
if self.activation_token_created:
if timezone.now() > self.activation_token_created + timedelta(hours=24):
@@ -174,6 +184,25 @@ 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"
diff --git a/engine/vibes_auth/templates/admin/verify_otp.html b/engine/vibes_auth/templates/admin/verify_otp.html
new file mode 100644
index 00000000..7bf58b30
--- /dev/null
+++ b/engine/vibes_auth/templates/admin/verify_otp.html
@@ -0,0 +1,31 @@
+{% 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 %}
+
+ {% include "unfold/helpers/messages/error.html" with errors=error %}
+
+ {% endif %}
+
+
+{% endblock %}
diff --git a/engine/vibes_auth/utils/emailing.py b/engine/vibes_auth/utils/emailing.py
index ce8a9d1c..8c506d09 100644
--- a/engine/vibes_auth/utils/emailing.py
+++ b/engine/vibes_auth/utils/emailing.py
@@ -19,6 +19,9 @@ 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")
@@ -27,7 +30,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(user.activation_token))}",
+ f"&token={urlsafe_base64_encode(force_bytes(raw_token))}",
"project_name": settings.PROJECT_NAME,
},
)
diff --git a/engine/vibes_auth/utils/otp.py b/engine/vibes_auth/utils/otp.py
new file mode 100644
index 00000000..8590b3ad
--- /dev/null
+++ b/engine/vibes_auth/utils/otp.py
@@ -0,0 +1,36 @@
+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)
diff --git a/engine/vibes_auth/viewsets.py b/engine/vibes_auth/viewsets.py
index 9c7a072c..ccddf821 100644
--- a/engine/vibes_auth/viewsets.py
+++ b/engine/vibes_auth/viewsets.py
@@ -139,8 +139,6 @@ 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(
{
diff --git a/pyproject.toml b/pyproject.toml
index 4526aae0..6493545b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -19,7 +19,7 @@ dependencies = [
"django-constance==4.3.4",
"django-cors-headers==4.9.0",
"django-dbbackup==5.2.0",
- "django-elasticsearch-dsl==8.2",
+ "django-elasticsearch-dsl==9.0",
"django-extensions==4.1",
"django-fernet-encrypted-fields==0.3.1",
"django-filter==25.2",
@@ -47,7 +47,6 @@ 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",
@@ -79,7 +78,7 @@ dependencies = [
[project.optional-dependencies]
worker = [
"celery==5.6.2",
- "django-celery-beat==2.8.1",
+ "django-celery-beat==2.9.0",
"django-celery-results==2.6.0",
]
linting = [
diff --git a/schon/settings/elasticsearch.py b/schon/settings/elasticsearch.py
index 4de5590e..b57d088b 100644
--- a/schon/settings/elasticsearch.py
+++ b/schon/settings/elasticsearch.py
@@ -5,9 +5,9 @@ from schon.settings.base import DEBUG
ELASTICSEARCH_DSL = {
"default": {
"hosts": ["http://elasticsearch:9200"],
- "http_auth": ("elastic", getenv("ELASTIC_PASSWORD")),
+ "basic_auth": ("elastic", getenv("ELASTIC_PASSWORD")),
"verify_certs": False,
- "timeout": 30,
+ "request_timeout": 30,
"ssl_show_warn": False,
"max_retries": 3,
"retry_on_timeout": True,
diff --git a/schon/utils/ratelimit.py b/schon/utils/ratelimit.py
new file mode 100644
index 00000000..ac315477
--- /dev/null
+++ b/schon/utils/ratelimit.py
@@ -0,0 +1,28 @@
+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
diff --git a/scripts/Unix/delete-elasticsearch.sh b/scripts/Unix/delete-elasticsearch.sh
new file mode 100644
index 00000000..e28519dc
--- /dev/null
+++ b/scripts/Unix/delete-elasticsearch.sh
@@ -0,0 +1,24 @@
+#!/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."
diff --git a/scripts/Windows/delete-elasticsearch.ps1 b/scripts/Windows/delete-elasticsearch.ps1
new file mode 100644
index 00000000..6cffdf45
--- /dev/null
+++ b/scripts/Windows/delete-elasticsearch.ps1
@@ -0,0 +1,38 @@
+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."
diff --git a/uv.lock b/uv.lock
index 883731c1..1331a679 100644
--- a/uv.lock
+++ b/uv.lock
@@ -634,14 +634,11 @@ wheels = [
[[package]]
name = "cron-descriptor"
-version = "2.0.6"
+version = "1.4.5"
source = { registry = "https://pypi.org/simple" }
-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" }
+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" }
wheels = [
- { 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" },
+ { 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" },
]
[[package]]
@@ -766,7 +763,7 @@ wheels = [
[[package]]
name = "django-celery-beat"
-version = "2.8.1"
+version = "2.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "celery" },
@@ -776,9 +773,9 @@ dependencies = [
{ name = "python-crontab" },
{ name = "tzdata" },
]
-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" }
+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" }
wheels = [
- { 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" },
+ { 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" },
]
[[package]]
@@ -843,15 +840,15 @@ wheels = [
[[package]]
name = "django-elasticsearch-dsl"
-version = "8.2"
+version = "9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "elasticsearch" },
{ name = "six" },
]
-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" }
+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" }
wheels = [
- { 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" },
+ { 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" },
]
[[package]]
@@ -1277,44 +1274,32 @@ wheels = [
[[package]]
name = "elastic-transport"
-version = "8.17.1"
+version = "9.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
+ { name = "sniffio" },
{ name = "urllib3" },
]
-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" }
+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" }
wheels = [
- { 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" },
+ { 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" },
]
[[package]]
name = "elasticsearch"
-version = "8.19.3"
+version = "9.3.0"
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/6b/79/365e306017a9fcfbbefab1a3b588d2404bea8806b36766ff0f886509a20e/elasticsearch-8.19.3.tar.gz", hash = "sha256:e84dd618a220cac25b962790085045dd27ac72e01c0a5d81bd29a2d47a71f03f", size = 800298, upload-time = "2025-12-23T12:56:00.72Z" }
+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" }
wheels = [
- { 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" },
+ { 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" },
]
[[package]]
@@ -3390,7 +3375,6 @@ dependencies = [
{ name = "drf-orjson-renderer" },
{ name = "drf-spectacular" },
{ name = "drf-spectacular-websocket" },
- { name = "elasticsearch-dsl" },
{ name = "filelock" },
{ name = "filetype" },
{ name = "graphene-django" },
@@ -3460,13 +3444,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.8.1" },
+ { name = "django-celery-beat", marker = "extra == 'worker'", specifier = "==2.9.0" },
{ 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 = "==8.2" },
+ { name = "django-elasticsearch-dsl", specifier = "==9.0" },
{ name = "django-extensions", specifier = "==4.1" },
{ name = "django-fernet-encrypted-fields", specifier = "==0.3.1" },
{ name = "django-filter", specifier = "==25.2" },
@@ -3495,7 +3479,6 @@ 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" },
@@ -4102,64 +4085,70 @@ wheels = [
[[package]]
name = "yarl"
-version = "1.22.0"
+version = "1.23.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
-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" }
+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" }
wheels = [
- { 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" },
+ { 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" },
]
[[package]]