Merge branch 'master' into storefront-nuxt

This commit is contained in:
Egor Pavlovich Gorbunov 2026-03-03 01:50:06 +03:00
commit f557fa462a
36 changed files with 744 additions and 163 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -494,7 +494,7 @@ class OrderType(DjangoObjectType):
description = _("orders") description = _("orders")
def resolve_total_price(self: Order, _info) -> float: 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: def resolve_total_quantity(self: Order, _info) -> int:
return self.total_quantity return self.total_quantity

View file

@ -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",
),
),
]

View file

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

View file

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

View file

@ -125,7 +125,7 @@ sitemap_detail.__doc__ = _( # ty:ignore[invalid-assignment]
_graphql_validation_rules = [QueryDepthLimitRule] _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) _graphql_validation_rules.append(NoSchemaIntrospectionCustomRule)

View file

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

View file

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

View file

@ -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),
),
]

View file

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

View file

@ -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 rest_framework.serializers import ModelSerializer, Serializer
from engine.payments.models import Transaction from engine.payments.models import Transaction
class DepositSerializer(Serializer): 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): class TransactionSerializer(ModelSerializer):
@ -32,5 +40,5 @@ class TransactionProcessSerializer(ModelSerializer):
class LimitsSerializer(Serializer): class LimitsSerializer(Serializer):
min_amount = FloatField(read_only=True) min_amount = DecimalField(max_digits=12, decimal_places=2, read_only=True)
max_amount = FloatField(read_only=True) max_amount = DecimalField(max_digits=12, decimal_places=2, read_only=True)

View file

@ -46,6 +46,7 @@ from engine.vibes_auth.emailing.tasks import (
) )
from engine.vibes_auth.forms import UserForm from engine.vibes_auth.forms import UserForm
from engine.vibes_auth.models import ( from engine.vibes_auth.models import (
AdminOTPCode,
BlacklistedToken, BlacklistedToken,
ChatMessage, ChatMessage,
ChatThread, ChatThread,
@ -403,6 +404,17 @@ class OutstandingTokenAdmin(BaseOutstandingTokenAdmin, ModelAdmin):
pass 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.register(User, UserAdmin)
admin.site.unregister(BaseGroup) admin.site.unregister(BaseGroup)

View file

@ -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)

View file

@ -11,4 +11,12 @@ class VibesAuthConfig(AppConfig):
hide = False hide = False
def ready(self) -> None: 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 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,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.utils.emailing import send_reset_password_email_task
from engine.vibes_auth.validators import is_valid_email, is_valid_phone_number from engine.vibes_auth.validators import is_valid_email, is_valid_phone_number
from schon.utils.ratelimit import graphql_ratelimit
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -49,6 +50,7 @@ class CreateUser(Mutation):
success = Boolean() success = Boolean()
@graphql_ratelimit(rate="5/h")
def mutate( def mutate(
self, self,
info, info,
@ -220,6 +222,7 @@ class ObtainJSONWebToken(Mutation):
refresh_token = String(required=True) refresh_token = String(required=True)
access_token = String(required=True) access_token = String(required=True)
@graphql_ratelimit(rate="10/h")
def mutate(self, info, email, password): def mutate(self, info, email, password):
serializer = TokenObtainPairSerializer( serializer = TokenObtainPairSerializer(
data={"email": email, "password": password}, retrieve_user=False data={"email": email, "password": password}, retrieve_user=False
@ -244,6 +247,7 @@ class RefreshJSONWebToken(Mutation):
user = Field(UserType) user = Field(UserType)
refresh_token = String() refresh_token = String()
@graphql_ratelimit(rate="10/h")
def mutate(self, info, refresh_token): def mutate(self, info, refresh_token):
serializer = TokenRefreshSerializer( serializer = TokenRefreshSerializer(
data={"refresh": refresh_token}, retrieve_user=False data={"refresh": refresh_token}, retrieve_user=False
@ -294,6 +298,7 @@ class ActivateUser(Mutation):
success = Boolean() success = Boolean()
@graphql_ratelimit(rate="5/h")
def mutate(self, info, uid, token): def mutate(self, info, uid, token):
try: try:
token = urlsafe_base64_decode(token).decode() token = urlsafe_base64_decode(token).decode()
@ -323,6 +328,7 @@ class ResetPassword(Mutation):
success = Boolean() success = Boolean()
@graphql_ratelimit(rate="4/h")
def mutate(self, info, email): def mutate(self, info, email):
try: try:
user = User.objects.get(email=email) user = User.objects.get(email=email)
@ -345,6 +351,7 @@ class ConfirmResetPassword(Mutation):
success = Boolean() success = Boolean()
@graphql_ratelimit(rate="5/h")
def mutate(self, info, uid, token, password, confirm_password): def mutate(self, info, uid, token, password, confirm_password):
try: try:
if not compare_digest(password, confirm_password): if not compare_digest(password, confirm_password):
@ -376,16 +383,28 @@ class ConfirmResetPassword(Mutation):
raise BadRequest(_(f"something went wrong: {e!s}")) from e 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 UploadAvatar(Mutation):
class Arguments: class Arguments:
file = Upload(required=True) file = Upload(required=True)
user = Field(UserType) user = Field(UserType)
@graphql_ratelimit(rate="3/h")
def mutate(self, info, file): def mutate(self, info, file):
if not info.context.user.is_authenticated: if not info.context.user.is_authenticated:
raise PermissionDenied(permission_denied_message) 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: try:
info.context.user.avatar = file info.context.user.avatar = file
info.context.user.save() info.context.user.save()

View file

View file

@ -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.'))

View file

@ -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.")

View file

@ -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",
},
),
]

View file

@ -94,7 +94,9 @@ class User(AbstractUser, NiceModel):
default=False, 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( activation_token_created = DateTimeField(
null=True, null=True,
blank=True, blank=True,
@ -148,17 +150,25 @@ class User(AbstractUser, NiceModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self._state.adding and self.activation_token_created is None: if self._state.adding and self.activation_token_created is None:
self.activation_token_created = timezone.now() self.activation_token_created = timezone.now()
if self._state.adding and not self.activation_token:
self.refresh_activation_token()
super().save(*args, **kwargs) super().save(*args, **kwargs)
def refresh_activation_token(self) -> None: def refresh_activation_token(self) -> str:
"""Generate a fresh activation token and update its timestamp.""" """Generate a fresh activation token, store its hash, return raw token."""
self.activation_token = uuid4() import hashlib
raw_token = str(uuid4())
self.activation_token = hashlib.sha256(raw_token.encode()).hexdigest()
self.activation_token_created = timezone.now() self.activation_token_created = timezone.now()
return raw_token
def check_token(self, token) -> bool: def check_token(self, token) -> bool:
import hashlib
from datetime import timedelta 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 return False
if self.activation_token_created: if self.activation_token_created:
if timezone.now() > self.activation_token_created + timedelta(hours=24): if timezone.now() > self.activation_token_created + timedelta(hours=24):
@ -174,6 +184,25 @@ class User(AbstractUser, NiceModel):
verbose_name_plural = _("users") 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): class ChatThread(NiceModel):
user = ForeignKey( user = ForeignKey(
User, null=True, blank=True, on_delete=SET_NULL, related_name="chat_threads" User, null=True, blank=True, on_delete=SET_NULL, related_name="chat_threads"

View file

@ -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 %}
<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,6 +19,9 @@ def send_verification_email_task(user_pk: str) -> tuple[bool, str]:
user = User.objects.get(pk=user_pk) user = User.objects.get(pk=user_pk)
user.refresh_from_db() user.refresh_from_db()
raw_token = user.refresh_activation_token()
user.save(update_fields=["activation_token", "activation_token_created"])
activate(user.language) activate(user.language)
email_subject = _(f"{settings.PROJECT_NAME} | Activate Account") 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, "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))}" "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, "project_name": settings.PROJECT_NAME,
}, },
) )

View file

@ -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)

View file

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

View file

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

View file

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

28
schon/utils/ratelimit.py Normal file
View file

@ -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

View file

@ -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."

View file

@ -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."

165
uv.lock
View file

@ -634,14 +634,11 @@ wheels = [
[[package]] [[package]]
name = "cron-descriptor" name = "cron-descriptor"
version = "2.0.6" version = "1.4.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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" }
{ 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 = [ 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]] [[package]]
@ -766,7 +763,7 @@ wheels = [
[[package]] [[package]]
name = "django-celery-beat" name = "django-celery-beat"
version = "2.8.1" version = "2.9.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "celery" }, { name = "celery" },
@ -776,9 +773,9 @@ dependencies = [
{ name = "python-crontab" }, { name = "python-crontab" },
{ name = "tzdata" }, { 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 = [ 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]] [[package]]
@ -843,15 +840,15 @@ wheels = [
[[package]] [[package]]
name = "django-elasticsearch-dsl" name = "django-elasticsearch-dsl"
version = "8.2" version = "9.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "elasticsearch" }, { name = "elasticsearch" },
{ name = "six" }, { 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 = [ 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]] [[package]]
@ -1277,44 +1274,32 @@ wheels = [
[[package]] [[package]]
name = "elastic-transport" name = "elastic-transport"
version = "8.17.1" version = "9.2.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
{ name = "sniffio" },
{ name = "urllib3" }, { 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 = [ 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]] [[package]]
name = "elasticsearch" name = "elasticsearch"
version = "8.19.3" version = "9.3.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "anyio" },
{ name = "elastic-transport" }, { name = "elastic-transport" },
{ name = "python-dateutil" }, { name = "python-dateutil" },
{ name = "sniffio" },
{ name = "typing-extensions" }, { 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 = [ 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" }, { 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]]
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]] [[package]]
@ -3390,7 +3375,6 @@ dependencies = [
{ name = "drf-orjson-renderer" }, { name = "drf-orjson-renderer" },
{ name = "drf-spectacular" }, { name = "drf-spectacular" },
{ name = "drf-spectacular-websocket" }, { name = "drf-spectacular-websocket" },
{ name = "elasticsearch-dsl" },
{ name = "filelock" }, { name = "filelock" },
{ name = "filetype" }, { name = "filetype" },
{ name = "graphene-django" }, { name = "graphene-django" },
@ -3460,13 +3444,13 @@ requires-dist = [
{ name = "cryptography", specifier = "==46.0.5" }, { name = "cryptography", specifier = "==46.0.5" },
{ name = "django", specifier = "==5.2.11" }, { name = "django", specifier = "==5.2.11" },
{ name = "django-cacheops", specifier = "==7.2" }, { 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-celery-results", marker = "extra == 'worker'", specifier = "==2.6.0" },
{ name = "django-constance", specifier = "==4.3.4" }, { name = "django-constance", specifier = "==4.3.4" },
{ name = "django-cors-headers", specifier = "==4.9.0" }, { name = "django-cors-headers", specifier = "==4.9.0" },
{ name = "django-dbbackup", specifier = "==5.2.0" }, { name = "django-dbbackup", specifier = "==5.2.0" },
{ name = "django-debug-toolbar", specifier = "==6.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-extensions", specifier = "==4.1" },
{ name = "django-fernet-encrypted-fields", specifier = "==0.3.1" }, { name = "django-fernet-encrypted-fields", specifier = "==0.3.1" },
{ name = "django-filter", specifier = "==25.2" }, { name = "django-filter", specifier = "==25.2" },
@ -3495,7 +3479,6 @@ requires-dist = [
{ name = "drf-orjson-renderer", specifier = "==1.8.0" }, { name = "drf-orjson-renderer", specifier = "==1.8.0" },
{ name = "drf-spectacular", specifier = "==0.29.0" }, { name = "drf-spectacular", specifier = "==0.29.0" },
{ name = "drf-spectacular-websocket", specifier = "==1.3.1" }, { name = "drf-spectacular-websocket", specifier = "==1.3.1" },
{ name = "elasticsearch-dsl", specifier = "==8.18.0" },
{ name = "filelock", specifier = "==3.25.0" }, { name = "filelock", specifier = "==3.25.0" },
{ name = "filetype", specifier = "==1.2.0" }, { name = "filetype", specifier = "==1.2.0" },
{ name = "graphene-django", specifier = "==3.2.3" }, { name = "graphene-django", specifier = "==3.2.3" },
@ -4102,64 +4085,70 @@ wheels = [
[[package]] [[package]]
name = "yarl" name = "yarl"
version = "1.22.0" version = "1.23.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "idna" }, { name = "idna" },
{ name = "multidict" }, { name = "multidict" },
{ name = "propcache" }, { 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 = [ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]] [[package]]