Features: 1) Improved request processing in middleware by adding mutable QueryDict implementation; 2) Extended type annotations across various modules for enhanced type safety; 3) Refined JWT token lifetime configuration for environment-specific logic.

Fixes: 1) Addressed missing or incorrect imports and type hints with `# ty:ignore` markers; 2) Fixed search queryset error handling in filters module; 3) Resolved issues in viewsets with updated `@action` method usage.

Extra: Removed unused classes and dependencies (e.g., `BaseMutation`, `basedpyright`, and related packages); streamlined GraphQL mutation implementations; cleaned up unused arguments in model `save` methods.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-12-19 15:17:17 +03:00
parent c3b4becc76
commit 13e7af52aa
26 changed files with 172 additions and 411 deletions

View file

@ -23,19 +23,17 @@ lint:
- "**/*.py" - "**/*.py"
- "pyproject.toml" - "pyproject.toml"
- ".pre-commit-config.yaml" - ".pre-commit-config.yaml"
- "pyrightconfig.json"
when: on_success when: on_success
- when: never - when: never
typecheck: typecheck:
stage: typecheck stage: typecheck
script: script:
- uv run pyright - uv run ty
rules: rules:
- changes: - changes:
- "**/*.py" - "**/*.py"
- "pyproject.toml" - "pyproject.toml"
- "pyrightconfig.json"
when: on_success when: on_success
- when: never - when: never

View file

@ -99,7 +99,7 @@ class Post(NiceModel):
verbose_name = _("post") verbose_name = _("post")
verbose_name_plural = _("posts") verbose_name_plural = _("posts")
def save(self, **kwargs): def save(self, *args, **kwargs):
if self.file: if self.file:
raise ValueError( raise ValueError(
_("markdown files are not supported yet - use markdown content instead") _("markdown files are not supported yet - use markdown content instead")
@ -110,7 +110,7 @@ class Post(NiceModel):
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
) )
) )
super().save(**kwargs) super().save(*args, **kwargs)
class PostTag(NiceModel): class PostTag(NiceModel):

View file

@ -31,13 +31,13 @@ class NiceModel(Model):
def save( def save(
self, self,
*, *args,
force_insert: bool = False, force_insert: bool = False,
force_update: bool = False, force_update: bool = False,
using: str | None = None, using: str | None = None,
update_fields: Collection[str] | None = None, update_fields: Collection[str] | None = None,
update_modified: bool = True, update_modified: bool = True,
) -> None: ) -> None: # ty:ignore[invalid-method-override]
self.update_modified = update_modified self.update_modified = update_modified
return super().save( return super().save(
force_insert=force_insert, force_insert=force_insert,

View file

@ -1,5 +1,5 @@
from contextlib import suppress from contextlib import suppress
from typing import Any, ClassVar, Type from typing import Any, Callable, ClassVar, Type
from constance.admin import Config from constance.admin import Config
from constance.admin import ConstanceAdmin as BaseConstanceAdmin from constance.admin import ConstanceAdmin as BaseConstanceAdmin
@ -145,6 +145,7 @@ class FieldsetsMixin:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
class ActivationActionsMixin: class ActivationActionsMixin:
message_user: Callable
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
actions = [ actions = [
@ -1086,10 +1087,8 @@ class ConstanceConfig:
_meta = Meta() _meta = Meta()
# noinspection PyTypeChecker site.unregister([Config]) # ty:ignore[invalid-argument-type]
site.unregister([Config]) site.register([ConstanceConfig], BaseConstanceAdmin) # ty:ignore[invalid-argument-type]
# noinspection PyTypeChecker
site.register([ConstanceConfig], BaseConstanceAdmin)
site.site_title = settings.PROJECT_NAME site.site_title = settings.PROJECT_NAME
site.site_header = "eVibes" site.site_header = "eVibes"
site.index_title = settings.PROJECT_NAME site.index_title = settings.PROJECT_NAME

View file

@ -10,6 +10,7 @@ from django_elasticsearch_dsl import fields
from django_elasticsearch_dsl.registries import registry from django_elasticsearch_dsl.registries import registry
from elasticsearch import NotFoundError from elasticsearch import NotFoundError
from elasticsearch.dsl import Q, Search from elasticsearch.dsl import Q, Search
from elasticsearch.dsl.types import Hit
from rest_framework.request import Request from rest_framework.request import Request
from engine.core.models import Brand, Category, Product from engine.core.models import Brand, Category, Product
@ -199,7 +200,7 @@ def process_query(
minimum_should_match=1, minimum_should_match=1,
) )
def build_search(idxs: list[str], size: int) -> Search: def build_search(idxs: list[str], size: int) -> Search[Hit]:
return ( return (
Search(index=idxs) Search(index=idxs)
.query(query_base) .query(query_base)

View file

@ -152,7 +152,7 @@ class ProductFilter(FilterSet):
data: dict[Any, Any] | None = None, data: dict[Any, Any] | None = None,
queryset: QuerySet[Product] | None = None, queryset: QuerySet[Product] | None = None,
*, *,
request: HttpRequest | Request | Context = None, request: HttpRequest | Request | Context | None = None,
prefix: str | None = None, prefix: str | None = None,
) -> None: ) -> None:
super().__init__(data=data, queryset=queryset, request=request, prefix=prefix) super().__init__(data=data, queryset=queryset, request=request, prefix=prefix)
@ -516,12 +516,12 @@ class CategoryFilter(FilterSet):
if not value: if not value:
return queryset return queryset
uuids = [ s_result = process_query(query=value, indexes=("categories",))
category.get("uuid")
for category in process_query(query=value, indexes=("categories",))[ if not s_result:
"categories" raise ValueError("Search is unprocessable")
]
] uuids = [category.get("uuid") for category in s_result.get("categories", [])]
return queryset.filter(uuid__in=uuids) return queryset.filter(uuid__in=uuids)

View file

@ -1,12 +0,0 @@
from typing import Any
from graphene import Mutation
class BaseMutation(Mutation):
def __init__(self, *args: list[Any], **kwargs: dict[Any, Any]) -> None:
super().__init__(*args, **kwargs)
@staticmethod
def mutate(**kwargs: Any) -> None:
pass

View file

@ -1,179 +0,0 @@
from contextlib import suppress
from django.core.exceptions import PermissionDenied
from django.utils.translation import gettext_lazy as _
from graphene import UUID, Boolean, Field, InputObjectType, List, NonNull, String
from engine.core.graphene import BaseMutation
from engine.core.graphene.object_types import ProductType
from engine.core.models import (
Attribute,
AttributeGroup,
AttributeValue,
Brand,
Category,
Product,
ProductTag,
)
from engine.core.utils.messages import permission_denied_message
def resolve_attributes(product, attributes):
for attr_input in attributes:
attribute = None
attr_uuid = attr_input.get("attribute_uuid")
if attr_uuid:
with suppress(Attribute.DoesNotExist):
attribute = Attribute.objects.get(uuid=attr_uuid)
if attribute is None:
group_name = attr_input.get("group_name")
attribute_name = attr_input.get("attribute_name")
value_type = attr_input.get("value_type") or "string"
if group_name and attribute_name:
group, _ = AttributeGroup.objects.get_or_create(name=group_name)
attribute, _ = Attribute.objects.get_or_create(
group=group,
name=attribute_name,
defaults={"value_type": value_type},
)
if attribute.value_type != value_type:
attribute.value_type = value_type
attribute.save(update_fields=["value_type"])
if attribute is not None:
AttributeValue.objects.update_or_create(
product=product,
attribute=attribute,
defaults={"value": str(attr_input.get("value", ""))},
)
def resolve_tags(product, tag_uuids):
tags = list(ProductTag.objects.filter(uuid__in=tag_uuids))
if tags:
product.tags.set(tags)
class AttributeInput(InputObjectType):
attribute_uuid = UUID(required=False, name="attributeUuid")
group_name = String(required=False, name="groupName")
attribute_name = String(required=False, name="attributeName")
value_type = String(required=False, name="valueType")
value = String(required=True)
class ProductInput(InputObjectType):
name = NonNull(String)
description = String(required=False)
is_digital = Boolean(required=False, name="isDigital")
partnumber = String(required=False)
sku = String(required=False)
category_uuid = NonNull(UUID, name="categoryUuid")
brand_uuid = UUID(required=False, name="brandUuid")
tag_uuids = List(UUID, required=False, name="tagUuids")
attributes = List(NonNull(AttributeInput), required=False)
# noinspection PyUnusedLocal,PyTypeChecker
class CreateProduct(BaseMutation):
class Meta:
description = _("create a product")
class Arguments:
product_data = NonNull(ProductInput, name="productData")
product = Field(ProductType)
@staticmethod
def mutate(parent, info, product_data):
user = info.context.user
if not user.has_perm("core.add_product"):
raise PermissionDenied(permission_denied_message)
category = Category.objects.get(uuid=product_data["category_uuid"])
brand = None
if product_data.get("brand_uuid"):
with suppress(Brand.DoesNotExist):
brand = Brand.objects.get(uuid=product_data["brand_uuid"])
product = Product.objects.create(
name=product_data["name"],
description=product_data.get("description"),
is_digital=product_data.get("is_digital") or False,
partnumber=product_data.get("partnumber"),
sku=product_data.get("sku") or None,
category=category,
brand=brand,
)
resolve_tags(product, product_data.get("tag_uuids", []))
resolve_attributes(product, product_data.get("attributes", []))
return CreateProduct(product=product)
# noinspection PyUnusedLocal,PyTypeChecker
class UpdateProduct(BaseMutation):
class Meta:
description = _("create a product")
class Arguments:
product_uuid = UUID(required=True)
product_data = NonNull(ProductInput, name="productData")
product = Field(ProductType)
@staticmethod
def mutate(parent, info, product_uuid, product_data):
user = info.context.user
if not user.has_perm("core.change_product"):
raise PermissionDenied(permission_denied_message)
product = Product.objects.get(uuid=product_uuid)
updates = {}
for field_in, model_field in (
("name", "name"),
("description", "description"),
("is_digital", "is_digital"),
("partnumber", "partnumber"),
("sku", "sku"),
):
if field_in in product_data and product_data[field_in] is not None:
updates[model_field] = product_data[field_in]
if product_data.get("category_uuid"):
product.category = Category.objects.get(uuid=product_data["category_uuid"])
if product_data.get("brand_uuid") is not None:
if product_data.get("brand_uuid"):
product.brand = Brand.objects.get(uuid=product_data["brand_uuid"])
else:
product.brand = None
for k, v in updates.items():
setattr(product, k, v)
product.save()
resolve_tags(product, product_data.get("tag_uuids", []))
resolve_attributes(product, product_data.get("attributes"))
return UpdateProduct(product=product)
# noinspection PyUnusedLocal,PyTypeChecker
class DeleteProduct(BaseMutation):
class Meta:
description = _("create a product")
class Arguments:
product_uuid = UUID(required=True)
ok = Boolean()
@staticmethod
def mutate(parent, info, product_uuid):
user = info.context.user
if not user.has_perm("core.delete_product"):
raise PermissionDenied(permission_denied_message)
Product.objects.get(uuid=product_uuid).delete()
return DeleteProduct(ok=True)

View file

@ -6,11 +6,10 @@ from django.core.cache import cache
from django.core.exceptions import BadRequest, PermissionDenied from django.core.exceptions import BadRequest, PermissionDenied
from django.http import Http404 from django.http import Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from graphene import UUID, Boolean, Field, Int, List, String from graphene import UUID, Boolean, Field, Int, List, Mutation, String
from graphene.types.generic import GenericScalar from graphene.types.generic import GenericScalar
from engine.core.elasticsearch import process_query from engine.core.elasticsearch import process_query
from engine.core.graphene import BaseMutation
from engine.core.graphene.object_types import ( from engine.core.graphene.object_types import (
AddressType, AddressType,
BulkProductInput, BulkProductInput,
@ -32,7 +31,7 @@ logger = logging.getLogger(__name__)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
class CacheOperator(BaseMutation): class CacheOperator(Mutation):
class Meta: class Meta:
description = _("cache I/O") description = _("cache I/O")
@ -54,7 +53,7 @@ class CacheOperator(BaseMutation):
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
class RequestCursedURL(BaseMutation): class RequestCursedURL(Mutation):
class Meta: class Meta:
description = _("request a CORSed URL") description = _("request a CORSed URL")
@ -82,7 +81,7 @@ class RequestCursedURL(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class AddOrderProduct(BaseMutation): class AddOrderProduct(Mutation):
class Meta: class Meta:
description = _("add a product to the order") description = _("add a product to the order")
@ -111,7 +110,7 @@ class AddOrderProduct(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class RemoveOrderProduct(BaseMutation): class RemoveOrderProduct(Mutation):
class Meta: class Meta:
description = _("remove a product from the order") description = _("remove a product from the order")
@ -140,7 +139,7 @@ class RemoveOrderProduct(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class RemoveAllOrderProducts(BaseMutation): class RemoveAllOrderProducts(Mutation):
class Meta: class Meta:
description = _("remove all products from the order") description = _("remove all products from the order")
@ -162,7 +161,7 @@ class RemoveAllOrderProducts(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class RemoveOrderProductsOfAKind(BaseMutation): class RemoveOrderProductsOfAKind(Mutation):
class Meta: class Meta:
description = _("remove a product from the order") description = _("remove a product from the order")
@ -185,7 +184,7 @@ class RemoveOrderProductsOfAKind(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class BuyOrder(BaseMutation): class BuyOrder(Mutation):
class Meta: class Meta:
description = _("buy an order") description = _("buy an order")
@ -256,7 +255,7 @@ class BuyOrder(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class BulkOrderAction(BaseMutation): class BulkOrderAction(Mutation):
class Meta: class Meta:
description = _("perform an action on a list of products in the order") description = _("perform an action on a list of products in the order")
@ -308,7 +307,7 @@ class BulkOrderAction(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class BulkWishlistAction(BaseMutation): class BulkWishlistAction(Mutation):
class Meta: class Meta:
description = _("perform an action on a list of products in the wishlist") description = _("perform an action on a list of products in the wishlist")
@ -349,7 +348,7 @@ class BulkWishlistAction(BaseMutation):
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
class BuyUnregisteredOrder(BaseMutation): class BuyUnregisteredOrder(Mutation):
class Meta: class Meta:
description = _("purchase an order without account creation") description = _("purchase an order without account creation")
@ -397,7 +396,7 @@ class BuyUnregisteredOrder(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class AddWishlistProduct(BaseMutation): class AddWishlistProduct(Mutation):
class Meta: class Meta:
description = _("add a product to the wishlist") description = _("add a product to the wishlist")
@ -425,7 +424,7 @@ class AddWishlistProduct(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class RemoveWishlistProduct(BaseMutation): class RemoveWishlistProduct(Mutation):
class Meta: class Meta:
description = _("remove a product from the wishlist") description = _("remove a product from the wishlist")
@ -453,7 +452,7 @@ class RemoveWishlistProduct(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class RemoveAllWishlistProducts(BaseMutation): class RemoveAllWishlistProducts(Mutation):
class Meta: class Meta:
description = _("remove all products from the wishlist") description = _("remove all products from the wishlist")
@ -481,7 +480,7 @@ class RemoveAllWishlistProducts(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class BuyWishlist(BaseMutation): class BuyWishlist(Mutation):
class Meta: class Meta:
description = _("buy all products from the wishlist") description = _("buy all products from the wishlist")
@ -531,7 +530,7 @@ class BuyWishlist(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class BuyProduct(BaseMutation): class BuyProduct(Mutation):
class Meta: class Meta:
description = _("buy a product") description = _("buy a product")
@ -576,7 +575,7 @@ class BuyProduct(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class FeedbackProductAction(BaseMutation): class FeedbackProductAction(Mutation):
class Meta: class Meta:
description = _("add or delete a feedback for orderproduct") description = _("add or delete a feedback for orderproduct")
@ -611,7 +610,7 @@ class FeedbackProductAction(BaseMutation):
# noinspection PyUnusedLocal,PyTypeChecker # noinspection PyUnusedLocal,PyTypeChecker
class CreateAddress(BaseMutation): class CreateAddress(Mutation):
class Arguments: class Arguments:
raw_data = String( raw_data = String(
required=True, description=_("original address string provided by the user") required=True, description=_("original address string provided by the user")
@ -628,7 +627,7 @@ class CreateAddress(BaseMutation):
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
class DeleteAddress(BaseMutation): class DeleteAddress(Mutation):
class Arguments: class Arguments:
uuid = UUID(required=True) uuid = UUID(required=True)
@ -655,7 +654,7 @@ class DeleteAddress(BaseMutation):
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
class AutocompleteAddress(BaseMutation): class AutocompleteAddress(Mutation):
class Arguments: class Arguments:
q = String() q = String()
limit = Int() limit = Int()
@ -676,7 +675,7 @@ class AutocompleteAddress(BaseMutation):
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
class ContactUs(BaseMutation): class ContactUs(Mutation):
class Arguments: class Arguments:
email = String(required=True) email = String(required=True)
name = String(required=True) name = String(required=True)
@ -707,7 +706,7 @@ class ContactUs(BaseMutation):
# noinspection PyArgumentList PyUnusedLocal # noinspection PyArgumentList PyUnusedLocal
class Search(BaseMutation): class Search(Mutation):
class Arguments: class Arguments:
query = String(required=True) query = String(required=True)

View file

@ -79,9 +79,6 @@ class AttributeGroup(ExportModelOperationsMixin("attribute_group"), NiceModel):
) )
is_publicly_visible = True is_publicly_visible = True
attributes: QuerySet["Attribute"]
children: QuerySet["Self"]
parent = ForeignKey( parent = ForeignKey(
"self", "self",
on_delete=CASCADE, on_delete=CASCADE,
@ -164,15 +161,7 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel):
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
def save( def save(self, *args, **kwargs) -> None:
self,
*,
force_insert: bool = False,
force_update: bool = False,
using: str | None = None,
update_fields: list[str] | tuple[str, ...] | None = None,
update_modified: bool = True,
) -> None:
users = self.users.filter(is_active=True) users = self.users.filter(is_active=True)
users = users.exclude(attributes__icontains="is_business") users = users.exclude(attributes__icontains="is_business")
if users.count() > 0: if users.count() > 0:
@ -182,11 +171,11 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel):
user.attributes.update({"is_business": True}) user.attributes.update({"is_business": True})
user.save() user.save()
return super().save( return super().save(
force_insert=force_insert, force_insert=kwargs.get("force_insert", False),
force_update=force_update, force_update=kwargs.get("force_update", False),
using=using, using=kwargs.get("using"),
update_fields=update_fields, update_fields=kwargs.get("update_fields"),
update_modified=update_modified, update_modified=kwargs.get("update_modified", True),
) )
class Meta: class Meta:
@ -704,7 +693,9 @@ class Product(ExportModelOperationsMixin("product"), NiceModel):
@cached_property @cached_property
def discount_price(self) -> float | None: def discount_price(self) -> float | None:
return self.promos.first().discount_percent if self.promos.exists() else None return (
self.promos.first().discount_percent if self.promos.exists() else None
) # ty:ignore[possibly-missing-attribute]
@property @property
def rating(self) -> float: def rating(self) -> float:

View file

@ -4,7 +4,6 @@ import os
import traceback import traceback
from contextlib import suppress from contextlib import suppress
from datetime import date, timedelta from datetime import date, timedelta
from typing import Any
import requests import requests
from constance import config from constance import config
@ -16,7 +15,6 @@ from django.core.exceptions import BadRequest
from django.db.models import Count, F, Sum from django.db.models import Count, F, Sum
from django.http import ( from django.http import (
FileResponse, FileResponse,
Http404,
HttpRequest, HttpRequest,
HttpResponse, HttpResponse,
HttpResponseRedirect, HttpResponseRedirect,
@ -30,6 +28,7 @@ from django.utils.http import urlsafe_base64_decode
from django.utils.timezone import now as tz_now from django.utils.timezone import now as tz_now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.vary import vary_on_headers from django.views.decorators.vary import vary_on_headers
from django_ratelimit.decorators import ratelimit from django_ratelimit.decorators import ratelimit
from drf_spectacular.utils import extend_schema_view from drf_spectacular.utils import extend_schema_view
@ -404,14 +403,13 @@ class DownloadDigitalAssetView(APIView):
) )
def favicon_view( @csrf_exempt
request: HttpRequest, *args: list[Any], **kwargs: dict[str, Any] def favicon_view(request: HttpRequest) -> HttpResponse | FileResponse:
) -> HttpResponse | FileResponse | None:
try: try:
favicon_path = os.path.join(settings.BASE_DIR, "static/favicon.png") favicon_path = os.path.join(settings.BASE_DIR, "static/favicon.png")
return FileResponse(open(favicon_path, "rb"), content_type="image/x-icon") return FileResponse(open(favicon_path, "rb"), content_type="image/x-icon")
except FileNotFoundError as fnfe: except FileNotFoundError:
raise Http404(_("favicon not found")) from fnfe return HttpResponse(status=404, content=_("favicon not found"))
# noinspection PyTypeChecker # noinspection PyTypeChecker

View file

@ -1,13 +1,13 @@
import graphene import graphene
from graphene import Mutation
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from engine.core.graphene import BaseMutation
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
class Deposit(BaseMutation): class Deposit(Mutation):
class Arguments: class Arguments:
amount = graphene.Float(required=True) amount = graphene.Float(required=True)

View file

@ -11,11 +11,10 @@ from django.db import IntegrityError
from django.http import Http404 from django.http import Http404
from django.utils.http import urlsafe_base64_decode from django.utils.http import urlsafe_base64_decode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from graphene import UUID, Boolean, Field, List, String from graphene import UUID, Boolean, Field, List, Mutation, String
from graphene.types.generic import GenericScalar from graphene.types.generic import GenericScalar
from graphene_file_upload.scalars import Upload from graphene_file_upload.scalars import Upload
from engine.core.graphene import BaseMutation
from engine.core.utils.messages import permission_denied_message from engine.core.utils.messages import permission_denied_message
from engine.core.utils.security import is_safe_key from engine.core.utils.security import is_safe_key
from engine.vibes_auth.graphene.object_types import UserType from engine.vibes_auth.graphene.object_types import UserType
@ -31,7 +30,7 @@ from engine.vibes_auth.validators import is_valid_email, is_valid_phone_number
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CreateUser(BaseMutation): class CreateUser(Mutation):
class Arguments: class Arguments:
email = String(required=True) email = String(required=True)
password = String(required=True) password = String(required=True)
@ -92,7 +91,7 @@ class CreateUser(BaseMutation):
raise BadRequest(str(e)) from e raise BadRequest(str(e)) from e
class UpdateUser(BaseMutation): class UpdateUser(Mutation):
class Arguments: class Arguments:
uuid = UUID(required=True) uuid = UUID(required=True)
email = String(required=False) email = String(required=False)
@ -187,7 +186,7 @@ class UpdateUser(BaseMutation):
raise BadRequest(str(e)) from e raise BadRequest(str(e)) from e
class DeleteUser(BaseMutation): class DeleteUser(Mutation):
class Arguments: class Arguments:
email = String() email = String()
uuid = UUID() uuid = UUID()
@ -212,7 +211,7 @@ class DeleteUser(BaseMutation):
raise PermissionDenied(permission_denied_message) raise PermissionDenied(permission_denied_message)
class ObtainJSONWebToken(BaseMutation): class ObtainJSONWebToken(Mutation):
class Arguments: class Arguments:
email = String(required=True) email = String(required=True)
password = String(required=True) password = String(required=True)
@ -236,7 +235,7 @@ class ObtainJSONWebToken(BaseMutation):
raise PermissionDenied(f"invalid credentials provided: {e!s}") from e raise PermissionDenied(f"invalid credentials provided: {e!s}") from e
class RefreshJSONWebToken(BaseMutation): class RefreshJSONWebToken(Mutation):
class Arguments: class Arguments:
refresh_token = String(required=True) refresh_token = String(required=True)
@ -259,7 +258,7 @@ class RefreshJSONWebToken(BaseMutation):
raise PermissionDenied(f"invalid refresh token provided: {e!s}") from e raise PermissionDenied(f"invalid refresh token provided: {e!s}") from e
class VerifyJSONWebToken(BaseMutation): class VerifyJSONWebToken(Mutation):
class Arguments: class Arguments:
token = String(required=True) token = String(required=True)
@ -281,7 +280,7 @@ class VerifyJSONWebToken(BaseMutation):
return VerifyJSONWebToken(token_is_valid=False, user=None, detail=detail) return VerifyJSONWebToken(token_is_valid=False, user=None, detail=detail)
class ActivateUser(BaseMutation): class ActivateUser(Mutation):
class Arguments: class Arguments:
uid = String(required=True) uid = String(required=True)
token = String(required=True) token = String(required=True)
@ -311,7 +310,7 @@ class ActivateUser(BaseMutation):
return ActivateUser(success=True) return ActivateUser(success=True)
class ResetPassword(BaseMutation): class ResetPassword(Mutation):
class Arguments: class Arguments:
email = String(required=True) email = String(required=True)
@ -330,7 +329,7 @@ class ResetPassword(BaseMutation):
return ResetPassword(success=True) return ResetPassword(success=True)
class ConfirmResetPassword(BaseMutation): class ConfirmResetPassword(Mutation):
class Arguments: class Arguments:
uid = String(required=True) uid = String(required=True)
token = String(required=True) token = String(required=True)
@ -370,7 +369,7 @@ class ConfirmResetPassword(BaseMutation):
raise BadRequest(_(f"something went wrong: {e!s}")) from e raise BadRequest(_(f"something went wrong: {e!s}")) from e
class UploadAvatar(BaseMutation): class UploadAvatar(Mutation):
class Arguments: class Arguments:
file = Upload(required=True) file = Upload(required=True)

View file

@ -233,7 +233,7 @@ class TokenRefreshSerializer(Serializer):
if api_settings.ROTATE_REFRESH_TOKENS: if api_settings.ROTATE_REFRESH_TOKENS:
if api_settings.BLACKLIST_AFTER_ROTATION: if api_settings.BLACKLIST_AFTER_ROTATION:
with suppress(AttributeError): with suppress(AttributeError):
refresh.blacklist() refresh.blacklist() # ty:ignore[possibly-missing-attribute]
refresh.set_jti() refresh.set_jti()
refresh.set_exp() refresh.set_exp()

View file

@ -2,7 +2,6 @@ import logging
import traceback import traceback
from contextlib import suppress from contextlib import suppress
from secrets import compare_digest from secrets import compare_digest
from typing import Type
from django.conf import settings from django.conf import settings
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.password_validation import validate_password
@ -53,7 +52,7 @@ class UserViewSet(
queryset = User.objects.filter(is_active=True) queryset = User.objects.filter(is_active=True)
permission_classes = [AllowAny] permission_classes = [AllowAny]
@action(detail=False, methods=["post"]) @action(detail=False, methods=("POST",))
@method_decorator( @method_decorator(
ratelimit(key="ip", rate="4/h" if not settings.DEBUG else "888/h") ratelimit(key="ip", rate="4/h" if not settings.DEBUG else "888/h")
) )
@ -65,7 +64,7 @@ class UserViewSet(
send_reset_password_email_task.delay(user_pk=str(user.uuid)) send_reset_password_email_task.delay(user_pk=str(user.uuid))
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
@action(detail=True, methods=["put"], permission_classes=[IsAuthenticated]) @action(detail=True, methods=("PUT",), permission_classes=[IsAuthenticated])
@method_decorator( @method_decorator(
ratelimit(key="ip", rate="3/h" if not settings.DEBUG else "888/h") ratelimit(key="ip", rate="3/h" if not settings.DEBUG else "888/h")
) )
@ -81,24 +80,25 @@ class UserViewSet(
) )
return Response(status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=["post"]) @action(detail=False, methods=("POST",))
@method_decorator( @method_decorator(
ratelimit(key="ip", rate="5/h" if not settings.DEBUG else "888/h") ratelimit(key="ip", rate="5/h" if not settings.DEBUG else "888/h")
) )
def confirm_password_reset(self, request: Request, *args, **kwargs) -> Response: def confirm_password_reset(self, request: Request, *args, **kwargs) -> Response:
try: try:
if not compare_digest( if not compare_digest(
request.data.get("password"), request.data.get("confirm_password") str(request.data.get("password")),
str(request.data.get("confirm_password")),
): ):
return Response( return Response(
{"error": _("passwords do not match")}, {"error": _("passwords do not match")},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
uuid = urlsafe_base64_decode(request.data.get("uidb_64")).decode() uuid = urlsafe_base64_decode(str(request.data.get("uidb_64"))).decode()
user = User.objects.get(pk=uuid) user = User.objects.get(pk=uuid)
validate_password(password=request.data.get("password"), user=user) validate_password(password=str(request.data.get("password")), user=user)
password_reset_token = PasswordResetTokenGenerator() password_reset_token = PasswordResetTokenGenerator()
if not password_reset_token.check_token(user, request.data.get("token")): if not password_reset_token.check_token(user, request.data.get("token")):
@ -139,18 +139,18 @@ class UserViewSet(
serializer.data, status=status.HTTP_201_CREATED, headers=headers serializer.data, status=status.HTTP_201_CREATED, headers=headers
) )
@action(detail=False, methods=["post"]) @action(detail=False, methods=("POST",))
@method_decorator( @method_decorator(
ratelimit(key="ip", rate="5/h" if not settings.DEBUG else "888/h") ratelimit(key="ip", rate="5/h" if not settings.DEBUG else "888/h")
) )
def activate(self, request: Request) -> Response: def activate(self, request: Request) -> Response:
detail = "" detail = ""
activation_error: Type[Exception] | None = None activation_error: Exception | None = None
try: try:
uuid = urlsafe_base64_decode(request.data.get("uidb_64")).decode() uuid = urlsafe_base64_decode(str(request.data.get("uidb_64"))).decode()
user = User.objects.get(pk=uuid) user = User.objects.get(pk=uuid)
if not user.check_token( if not user.check_token(
urlsafe_base64_decode(request.data.get("token")).decode() urlsafe_base64_decode(str(request.data.get("token"))).decode()
): ):
return Response( return Response(
{"error": _("activation link is invalid!")}, {"error": _("activation link is invalid!")},
@ -175,7 +175,7 @@ class UserViewSet(
detail = str(traceback.format_exc()) detail = str(traceback.format_exc())
if user is None: if user is None:
if settings.DEBUG: if settings.DEBUG:
raise Exception from activation_error raise Exception from activation_error # ty:ignore[possibly-unresolved-reference]
return Response( return Response(
{"error": _("activation link is invalid!"), "detail": detail}, {"error": _("activation link is invalid!"), "detail": detail},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -187,7 +187,7 @@ class UserViewSet(
response_data["access"] = str(tokens.access_token) response_data["access"] = str(tokens.access_token)
return Response(response_data, status=status.HTTP_200_OK) return Response(response_data, status=status.HTTP_200_OK)
@action(detail=True, methods=["put"], permission_classes=[IsAuthenticated]) @action(detail=True, methods=("PUT",), permission_classes=[IsAuthenticated])
def merge_recently_viewed(self, request: Request, *args, **kwargs) -> Response: def merge_recently_viewed(self, request: Request, *args, **kwargs) -> Response:
user = self.get_object() user = self.get_object()
if request.user != user: if request.user != user:

View file

@ -1,15 +0,0 @@
from typing import Any
from urllib.parse import urlparse
from storages.backends.ftp import FTPStorage
class AbsoluteFTPStorage(FTPStorage):
# noinspection PyProtectedMember
# noinspection PyUnresolvedReferences
def _get_config(self) -> Any:
cfg = super()._get_config()
url = urlparse(self.location)
cfg["path"] = url.path or cfg["path"]
return cfg

View file

@ -17,6 +17,7 @@ from django.http import (
HttpResponseForbidden, HttpResponseForbidden,
HttpResponsePermanentRedirect, HttpResponsePermanentRedirect,
JsonResponse, JsonResponse,
QueryDict,
) )
from django.middleware.common import CommonMiddleware from django.middleware.common import CommonMiddleware
from django.middleware.locale import LocaleMiddleware from django.middleware.locale import LocaleMiddleware
@ -177,10 +178,21 @@ class CamelCaseMiddleWare:
self.get_response = get_response self.get_response = get_response
def __call__(self, request): def __call__(self, request):
request.GET = underscoreize( underscoreized_get = underscoreize(
request.GET, {k: v for k, v in request.GET.lists()},
**JSON_UNDERSCOREIZE, **JSON_UNDERSCOREIZE,
) )
new_get = QueryDict(mutable=True)
for key, value in underscoreized_get.items():
if isinstance(value, list):
for val in value:
new_get.appendlist(key, val)
else:
new_get[key] = value
new_get._mutable = False
request.GET = new_get
response = self.get_response(request) response = self.get_response(request)
return response return response

View file

@ -10,6 +10,8 @@ class CustomPagination(PageNumberPagination):
) )
def get_paginated_response(self, data: dict[str, Any]) -> Response: def get_paginated_response(self, data: dict[str, Any]) -> Response:
if not self.page:
raise RuntimeError
return Response( return Response(
{ {
"links": { "links": {
@ -25,9 +27,7 @@ class CustomPagination(PageNumberPagination):
} }
) )
def get_paginated_response_schema( def get_paginated_response_schema(self, schema: dict[str, Any]) -> dict[str, Any]:
self, data_schema: dict[str, Any]
) -> dict[str, Any]:
return { return {
"type": "object", "type": "object",
"properties": { "properties": {
@ -63,6 +63,6 @@ class CustomPagination(PageNumberPagination):
"example": 100, "example": 100,
"description": "Total number of items", "description": "Total number of items",
}, },
"data": data_schema, "data": schema,
}, },
} }

View file

@ -348,7 +348,7 @@ LANGUAGE_URL_OVERRIDES: dict[str, str] = {
code.split("-")[0]: code for code, _ in LANGUAGES if "-" in code code.split("-")[0]: code for code, _ in LANGUAGES if "-" in code
} }
CURRENCY_CODE: str = dict(CURRENCIES_BY_LANGUAGES).get(LANGUAGE_CODE) CURRENCY_CODE: str = dict(CURRENCIES_BY_LANGUAGES).get(LANGUAGE_CODE, "")
MODELTRANSLATION_FALLBACK_LANGUAGES: tuple[str, ...] = (LANGUAGE_CODE, "en-us", "de-de") MODELTRANSLATION_FALLBACK_LANGUAGES: tuple[str, ...] = (LANGUAGE_CODE, "en-us", "de-de")
@ -517,7 +517,7 @@ if getenv("DBBACKUP_HOST") and getenv("DBBACKUP_USER") and getenv("DBBACKUP_PASS
STORAGES.update( STORAGES.update(
{ {
"dbbackup": { "dbbackup": {
"BACKEND": "evibes.ftpstorage.AbsoluteFTPStorage", "BACKEND": "storages.backends.ftp.FTPStorage",
"OPTIONS": { "OPTIONS": {
"location": ( "location": (
f"ftp://{getenv('DBBACKUP_USER')}:{getenv('DBBACKUP_PASS')}@{getenv('DBBACKUP_HOST')}:21/{raw_path}" f"ftp://{getenv('DBBACKUP_USER')}:{getenv('DBBACKUP_PASS')}@{getenv('DBBACKUP_HOST')}:21/{raw_path}"

View file

@ -1,5 +1,6 @@
from datetime import timedelta from datetime import timedelta
from os import getenv from os import getenv
from typing import Any
from django.utils.text import format_lazy from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -12,7 +13,7 @@ from evibes.settings.base import (
SECRET_KEY, SECRET_KEY,
) )
REST_FRAMEWORK: dict[str, str | int | list[str] | tuple[str, ...] | dict[str, bool]] = { REST_FRAMEWORK: dict[str, Any] = {
"DEFAULT_PAGINATION_CLASS": "evibes.pagination.CustomPagination", "DEFAULT_PAGINATION_CLASS": "evibes.pagination.CustomPagination",
"PAGE_SIZE": 30, "PAGE_SIZE": 30,
"DEFAULT_AUTHENTICATION_CLASSES": [ "DEFAULT_AUTHENTICATION_CLASSES": [
@ -30,11 +31,14 @@ REST_FRAMEWORK: dict[str, str | int | list[str] | tuple[str, ...] | dict[str, bo
}, },
} }
JSON_UNDERSCOREIZE = REST_FRAMEWORK.get("JSON_UNDERSCOREIZE", {}) JSON_UNDERSCOREIZE: dict[str, Any] = REST_FRAMEWORK.get("JSON_UNDERSCOREIZE", ())
access_lifetime = timedelta(hours=8) if not DEBUG else timedelta(hours=88)
refresh_lifetime = timedelta(days=8)
SIMPLE_JWT: dict[str, timedelta | str | bool] = { SIMPLE_JWT: dict[str, timedelta | str | bool] = {
"ACCESS_TOKEN_LIFETIME": timedelta(hours=8) if not DEBUG else timedelta(hours=88), "ACCESS_TOKEN_LIFETIME": access_lifetime,
"REFRESH_TOKEN_LIFETIME": timedelta(days=8), "REFRESH_TOKEN_LIFETIME": refresh_lifetime,
"ROTATE_REFRESH_TOKENS": True, "ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True, "BLACKLIST_AFTER_ROTATION": True,
"UPDATE_LAST_LOGIN": True, "UPDATE_LAST_LOGIN": True,
@ -90,15 +94,14 @@ The API supports multiple response formats:
Current API version: %(version)s Current API version: %(version)s
""") # noqa: E501, F405 """) # noqa: E501, F405
_access_seconds = SIMPLE_JWT.get("ACCESS_TOKEN_LIFETIME").total_seconds()
if not DEBUG: if not DEBUG:
_access_lifetime = int(_access_seconds // 60) _access_lifetime = int(access_lifetime.total_seconds() // 60)
_access_unit = "minutes" _access_unit = "minutes"
else: else:
_access_lifetime = int(_access_seconds // 3600) _access_lifetime = int(access_lifetime.total_seconds() // 3600)
_access_unit = "hours" _access_unit = "hours"
_refresh_hours = int(SIMPLE_JWT.get("REFRESH_TOKEN_LIFETIME").total_seconds() // 3600) _refresh_hours = int(refresh_lifetime.total_seconds() // 3600)
SPECTACULAR_DESCRIPTION = format_lazy( SPECTACULAR_DESCRIPTION = format_lazy(
_SPECTACULAR_DESCRIPTION_TEMPLATE, _SPECTACULAR_DESCRIPTION_TEMPLATE,

View file

@ -1,6 +1,8 @@
from django.db import models from django.db import models
from django_elasticsearch_dsl.registries import registry from django_elasticsearch_dsl.registries import registry
from django_elasticsearch_dsl.signals import CelerySignalProcessor from django_elasticsearch_dsl.signals import (
CelerySignalProcessor, # ty:ignore[possibly-missing-import]
)
class SelectiveSignalProcessor(CelerySignalProcessor): class SelectiveSignalProcessor(CelerySignalProcessor):

View file

@ -48,6 +48,7 @@ urlpatterns = [
path( path(
r"favicon.ico", r"favicon.ico",
favicon_view, favicon_view,
name="favicon",
), ),
path( path(
r"graphql/", r"graphql/",

View file

@ -1,5 +1,5 @@
import re import re
from typing import Any, MutableMapping from typing import Any, Collection, MutableMapping
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from drf_orjson_renderer.renderers import ORJSONRenderer from drf_orjson_renderer.renderers import ORJSONRenderer
@ -43,12 +43,12 @@ def camelize(obj: Any) -> Any:
def camelize_serializer_fields(result, generator, request, public): def camelize_serializer_fields(result, generator, request, public):
ignore_fields = JSON_UNDERSCOREIZE.get("ignore_fields") or () ignore_fields: Collection[Any] = JSON_UNDERSCOREIZE.get("ignore_fields", ())
ignore_keys = JSON_UNDERSCOREIZE.get("ignore_keys") or () ignore_keys: Collection[Any] = JSON_UNDERSCOREIZE.get("ignore_keys", ())
def has_middleware_installed(): def has_middleware_installed():
try: try:
from djangorestframework_camel_case.middleware import CamelCaseMiddleWare from evibes.middleware import CamelCaseMiddleWare
except ImportError: except ImportError:
return False return False
@ -104,7 +104,7 @@ def camelize_serializer_fields(result, generator, request, public):
class CamelCaseRenderer(ORJSONRenderer): class CamelCaseRenderer(ORJSONRenderer):
def render( def render(
self, data: Any, media_type: str | None = None, renderer_context: Any = None self, data: Any, media_type: str | None = None, renderer_context: Any = None
) -> bytes: ) -> bytes: # ty:ignore[invalid-method-override]
if data is None: if data is None:
return b"" return b""

View file

@ -86,9 +86,8 @@ worker = [
"celery-prometheus-exporter==1.7.0", "celery-prometheus-exporter==1.7.0",
] ]
linting = [ linting = [
"ty==0.0.3",
"ruff==0.14.9", "ruff==0.14.9",
"basedpyright>=1.36.1",
"pyright==1.1.407",
"celery-types>=0.23.0", "celery-types>=0.23.0",
"django-stubs==5.2.8", "django-stubs==5.2.8",
"djangorestframework-stubs==3.16.6", "djangorestframework-stubs==3.16.6",
@ -112,7 +111,15 @@ python-version = "3.13"
[tool.ruff] [tool.ruff]
line-length = 88 line-length = 88
target-version = "py312" target-version = "py312"
exclude = ["media", "static", "storefront"] exclude = [
"Dockerfiles",
"monitoring",
"scripts",
"static",
"storefront",
"tmp",
"media",
]
[tool.ruff.lint] [tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "B", "Q", "I"] select = ["E4", "E7", "E9", "F", "B", "Q", "I"]
@ -125,41 +132,25 @@ known-first-party = ["evibes", "engine"]
quote-style = "double" quote-style = "double"
indent-style = "space" indent-style = "space"
[tool.pyright] [tool.ty.environment]
typeCheckingMode = "strict" python-version = "3.12"
pythonVersion = "3.12"
useLibraryCodeForTypes = true
reportMissingTypeStubs = true
reportGeneralTypeIssues = false
reportRedeclaration = false
exclude = [
"**/__pycache__/**",
"**/.venv/**",
"**/.uv/**",
"media/**",
"static/**",
"storefront/**",
"**/migrations/**",
]
extraPaths = ["./evibes", "./engine"]
[tool.basedpyright] [tool.ty.terminal]
typeCheckingMode = "strict" output-format = "concise"
pythonVersion = "3.12"
useLibraryCodeForTypes = true [tool.ty.rules]
reportMissingTypeStubs = true possibly-unresolved-reference = "warn"
reportGeneralTypeIssues = false
reportRedeclaration = false [[tool.ty.overrides]]
exclude = [ exclude = [
"**/__pycache__/**", "Dockerfiles/**",
"**/.venv/**", "monitoring/**",
"**/.uv/**", "scripts/**",
"media/**",
"static/**", "static/**",
"storefront/**", "storefront/**",
"**/migrations/**", "tmp/**",
"media/**",
] ]
extraPaths = ["./evibes", "./engine"]
[tool.django-stubs] [tool.django-stubs]
django_settings_module = "evibes.settings.__init__" django_settings_module = "evibes.settings.__init__"

81
uv.lock
View file

@ -257,18 +257,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
] ]
[[package]]
name = "basedpyright"
version = "1.36.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodejs-wheel-binaries" },
]
sdist = { url = "https://files.pythonhosted.org/packages/32/29/d42d543a1637e692ac557bfc6d6fcf50e9a7061c1cb4da403378d6a70453/basedpyright-1.36.1.tar.gz", hash = "sha256:20c9a24e2a4c95d5b6d46c78a6b6c7e3dc7cbba227125256431d47c595b15fd4", size = 22834851, upload-time = "2025-12-11T14:55:47.463Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/7f/f0133313bffa303d32aa74468981eb6b2da7fadda6247c9aa0aeab8391b1/basedpyright-1.36.1-py3-none-any.whl", hash = "sha256:3d738484fe9681cdfe35dd98261f30a9a7aec64208bc91f8773a9aaa9b89dd16", size = 11881725, upload-time = "2025-12-11T14:55:43.805Z" },
]
[[package]] [[package]]
name = "bcrypt" name = "bcrypt"
version = "5.0.0" version = "5.0.0"
@ -1420,12 +1408,11 @@ jupyter = [
{ name = "jupyter" }, { name = "jupyter" },
] ]
linting = [ linting = [
{ name = "basedpyright" },
{ name = "celery-types" }, { name = "celery-types" },
{ name = "django-stubs" }, { name = "django-stubs" },
{ name = "djangorestframework-stubs" }, { name = "djangorestframework-stubs" },
{ name = "pyright" },
{ name = "ruff" }, { name = "ruff" },
{ name = "ty" },
{ name = "types-docutils" }, { name = "types-docutils" },
{ name = "types-paramiko" }, { name = "types-paramiko" },
{ name = "types-pillow" }, { name = "types-pillow" },
@ -1448,7 +1435,6 @@ worker = [
requires-dist = [ requires-dist = [
{ name = "aiogram", specifier = "==3.23.0" }, { name = "aiogram", specifier = "==3.23.0" },
{ name = "aiosmtpd", specifier = "==1.4.6" }, { name = "aiosmtpd", specifier = "==1.4.6" },
{ name = "basedpyright", marker = "extra == 'linting'", specifier = ">=1.36.1" },
{ name = "celery", marker = "extra == 'worker'", specifier = "==5.6.0" }, { name = "celery", marker = "extra == 'worker'", specifier = "==5.6.0" },
{ name = "celery-prometheus-exporter", marker = "extra == 'worker'", specifier = "==1.7.0" }, { name = "celery-prometheus-exporter", marker = "extra == 'worker'", specifier = "==1.7.0" },
{ name = "celery-types", marker = "extra == 'linting'", specifier = ">=0.23.0" }, { name = "celery-types", marker = "extra == 'linting'", specifier = ">=0.23.0" },
@ -1514,7 +1500,6 @@ requires-dist = [
{ name = "pygraphviz", marker = "sys_platform != 'win32' and extra == 'graph'", specifier = "==1.14" }, { name = "pygraphviz", marker = "sys_platform != 'win32' and extra == 'graph'", specifier = "==1.14" },
{ name = "pyjwt", specifier = "==2.10.1" }, { name = "pyjwt", specifier = "==2.10.1" },
{ name = "pymdown-extensions", specifier = "==10.19.1" }, { name = "pymdown-extensions", specifier = "==10.19.1" },
{ name = "pyright", marker = "extra == 'linting'", specifier = "==1.1.407" },
{ name = "pytest", specifier = "==9.0.2" }, { name = "pytest", specifier = "==9.0.2" },
{ name = "pytest-django", specifier = "==4.11.1" }, { name = "pytest-django", specifier = "==4.11.1" },
{ name = "python-slugify", specifier = "==8.0.4" }, { name = "python-slugify", specifier = "==8.0.4" },
@ -1524,6 +1509,7 @@ requires-dist = [
{ name = "sentry-sdk", extras = ["django", "celery", "opentelemetry"], specifier = "==2.48.0" }, { name = "sentry-sdk", extras = ["django", "celery", "opentelemetry"], specifier = "==2.48.0" },
{ name = "six", specifier = "==1.17.0" }, { name = "six", specifier = "==1.17.0" },
{ name = "swapper", specifier = "==1.4.0" }, { name = "swapper", specifier = "==1.4.0" },
{ name = "ty", marker = "extra == 'linting'", specifier = "==0.0.3" },
{ name = "types-docutils", marker = "extra == 'linting'", specifier = "==0.22.3.20251115" }, { name = "types-docutils", marker = "extra == 'linting'", specifier = "==0.22.3.20251115" },
{ name = "types-paramiko", marker = "extra == 'linting'", specifier = "==4.0.0.20250822" }, { name = "types-paramiko", marker = "extra == 'linting'", specifier = "==4.0.0.20250822" },
{ name = "types-pillow", marker = "extra == 'linting'", specifier = "==10.2.0.20240822" }, { name = "types-pillow", marker = "extra == 'linting'", specifier = "==10.2.0.20240822" },
@ -2541,31 +2527,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
] ]
[[package]]
name = "nodeenv"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
]
[[package]]
name = "nodejs-wheel-binaries"
version = "24.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b9/35/d806c2ca66072e36dc340ccdbeb2af7e4f1b5bcc33f1481f00ceed476708/nodejs_wheel_binaries-24.12.0.tar.gz", hash = "sha256:f1b50aa25375e264697dec04b232474906b997c2630c8f499f4caf3692938435", size = 8058, upload-time = "2025-12-11T21:12:26.856Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/3b/9d6f044319cd5b1e98f07c41e2465b58cadc1c9c04a74c891578f3be6cb5/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:7564ddea0a87eff34e9b3ef71764cc2a476a8f09a5cccfddc4691148b0a47338", size = 55125859, upload-time = "2025-12-11T21:11:58.132Z" },
{ url = "https://files.pythonhosted.org/packages/48/a5/f5722bf15c014e2f476d7c76bce3d55c341d19122d8a5d86454db32a61a4/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:8ff929c4669e64613ceb07f5bbd758d528c3563820c75d5de3249eb452c0c0ab", size = 55309035, upload-time = "2025-12-11T21:12:01.754Z" },
{ url = "https://files.pythonhosted.org/packages/a9/61/68d39a6f1b5df67805969fd2829ba7e80696c9af19537856ec912050a2be/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:6ebacefa8891bc456ad3655e6bce0af7e20ba08662f79d9109986faeb703fd6f", size = 59661017, upload-time = "2025-12-11T21:12:05.268Z" },
{ url = "https://files.pythonhosted.org/packages/16/a1/31aad16f55a5e44ca7ea62d1367fc69f4b6e1dba67f58a0a41d0ed854540/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3292649a03682ccbfa47f7b04d3e4240e8c46ef04dc941b708f20e4e6a764f75", size = 60159770, upload-time = "2025-12-11T21:12:08.696Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5e/b7c569aa1862690ca4d4daf3a64cafa1ea6ce667a9e3ae3918c56e127d9b/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7fb83df312955ea355ba7f8cbd7055c477249a131d3cb43b60e4aeb8f8c730b1", size = 61653561, upload-time = "2025-12-11T21:12:12.575Z" },
{ url = "https://files.pythonhosted.org/packages/71/87/567f58d7ba69ff0208be849b37be0f2c2e99c69e49334edd45ff44f00043/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2473c819448fedd7b036dde236b09f3c8bbf39fbbd0c1068790a0498800f498b", size = 62238331, upload-time = "2025-12-11T21:12:16.143Z" },
{ url = "https://files.pythonhosted.org/packages/6a/9d/c6492188ce8de90093c6755a4a63bb6b2b4efb17094cb4f9a9a49c73ed3b/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_amd64.whl", hash = "sha256:2090d59f75a68079fabc9b86b14df8238b9aecb9577966dc142ce2a23a32e9bb", size = 41342076, upload-time = "2025-12-11T21:12:20.618Z" },
{ url = "https://files.pythonhosted.org/packages/df/af/cd3290a647df567645353feed451ef4feaf5844496ced69c4dcb84295ff4/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_arm64.whl", hash = "sha256:d0c2273b667dd7e3f55e369c0085957b702144b1b04bfceb7ce2411e58333757", size = 39048104, upload-time = "2025-12-11T21:12:23.495Z" },
]
[[package]] [[package]]
name = "notebook" name = "notebook"
version = "7.5.1" version = "7.5.1"
@ -3220,19 +3181,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/35/76/c34426d532e4dce7ff36e4d92cb20f4cbbd94b619964b93d24e8f5b5510f/pynacl-1.6.1-cp38-abi3-win_arm64.whl", hash = "sha256:5953e8b8cfadb10889a6e7bd0f53041a745d1b3d30111386a1bb37af171e6daf", size = 183970, upload-time = "2025-11-10T16:02:05.786Z" }, { url = "https://files.pythonhosted.org/packages/35/76/c34426d532e4dce7ff36e4d92cb20f4cbbd94b619964b93d24e8f5b5510f/pynacl-1.6.1-cp38-abi3-win_arm64.whl", hash = "sha256:5953e8b8cfadb10889a6e7bd0f53041a745d1b3d30111386a1bb37af171e6daf", size = 183970, upload-time = "2025-11-10T16:02:05.786Z" },
] ]
[[package]]
name = "pyright"
version = "1.1.407"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872, upload-time = "2025-10-24T23:17:15.145Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" },
]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "9.0.2" version = "9.0.2"
@ -3762,6 +3710,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
] ]
[[package]]
name = "ty"
version = "0.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/cd/aee86c0da3240960d6b7e807f3a41c89bae741495d81ca303200b0103dc9/ty-0.0.3.tar.gz", hash = "sha256:831259e22d3855436701472d4c0da200cd45041bc677eae79415d684f541de8a", size = 4769098, upload-time = "2025-12-18T02:16:49.773Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/ef/2d0d18e8fe6b673d3e1ea642f18404d7edfa9d08310f7203e8f0e7dc862e/ty-0.0.3-py3-none-linux_armv6l.whl", hash = "sha256:cd035bb75acecb78ac1ba8c4cc696f57a586e29d36e84bd691bc3b5b8362794c", size = 9763890, upload-time = "2025-12-18T02:16:56.879Z" },
{ url = "https://files.pythonhosted.org/packages/bb/67/0ae31574619a7264df8cf8e641f246992db22ac1720c2a72953aa31cbe61/ty-0.0.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7708eaf73485e263efc7ef339f8e4487d3f5885779edbeec504fd72e4521c376", size = 9558276, upload-time = "2025-12-18T02:16:45.453Z" },
{ url = "https://files.pythonhosted.org/packages/d7/f7/3b9c033e80910972fca3783e4a52ba9cb7cd5c8b6828a87986646d64082b/ty-0.0.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3113a633f46ec789f6df675b7afc5d3ab20c247c92ae4dbb9aa5b704768c18b2", size = 9094451, upload-time = "2025-12-18T02:17:01.155Z" },
{ url = "https://files.pythonhosted.org/packages/9a/29/9a90ed6bef00142a088965100b5e0a5d11805b9729c151ca598331bbd92b/ty-0.0.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a451f3f73a04bf18e551b1ebebb79b20fac5f09740a353f7e07b5f607b217c4f", size = 9568049, upload-time = "2025-12-18T02:16:28.643Z" },
{ url = "https://files.pythonhosted.org/packages/2f/ab/8daeb12912c2de8a3154db652931f4ad0d27c555faebcaf34af08bcfd0d2/ty-0.0.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f6e926b6de0becf0452e1afad75cb71f889a4777cd14269e5447d46c01b2770", size = 9547711, upload-time = "2025-12-18T02:16:54.464Z" },
{ url = "https://files.pythonhosted.org/packages/91/54/f5c1f293f647beda717fee2448cc927ac0d05f66bebe18647680a67e1d67/ty-0.0.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160e7974150f9f359c31d5808214676d1baa05321ab5a7b29fb09f4906dbdb38", size = 9983225, upload-time = "2025-12-18T02:17:05.672Z" },
{ url = "https://files.pythonhosted.org/packages/95/34/065962cfa2e87c10db839512229940a366b8ca1caffa2254a277b1694e5a/ty-0.0.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:726576df31d4e76934ffc64f2939d4a9bc195c7427452c8c159261ad00bd1b5e", size = 10851148, upload-time = "2025-12-18T02:16:38.354Z" },
{ url = "https://files.pythonhosted.org/packages/54/27/e2a8cbfc33999eef882ccd1b816ed615293f96e96f6df60cd12f84b69ca2/ty-0.0.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5014cf4744c94d9ea7b43314199ddaf52564a80b3d006e4ba0fe982bc42f4e8b", size = 10564441, upload-time = "2025-12-18T02:17:03.584Z" },
{ url = "https://files.pythonhosted.org/packages/91/6d/dcce3e222e59477c1f2b3a012cc76428d7032248138cd5544ad7f1cda7bd/ty-0.0.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a9a51dc040f2718725f34ae6ef51fe8f8bd689e21bd3e82f4e71767034928de", size = 10358651, upload-time = "2025-12-18T02:16:26.091Z" },
{ url = "https://files.pythonhosted.org/packages/53/36/b6d0154b83a5997d607bf1238200271c17223f68aab2c778ded5424f9c1e/ty-0.0.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e6188eddd3a228c449261bb398e8621d33b92c1fc03599afdfad4388327a48", size = 10120457, upload-time = "2025-12-18T02:16:51.864Z" },
{ url = "https://files.pythonhosted.org/packages/cc/46/05dc826674ee1a451406e4c253c71700a6f707bae88b706a4c9e9bba6919/ty-0.0.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5cc55e08d5d18edf1c5051af02456bd359716f07aae0a305e4cefe7735188540", size = 9551642, upload-time = "2025-12-18T02:16:33.518Z" },
{ url = "https://files.pythonhosted.org/packages/64/8a/f90b60d103fd5ec04ecbac091a64e607e6cd37cec6e718bba17cb2022644/ty-0.0.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:34b2d589412a81d1fd6d7fe461353068496c2bf1f7113742bd6d88d1d57ec3ad", size = 9572234, upload-time = "2025-12-18T02:16:31.013Z" },
{ url = "https://files.pythonhosted.org/packages/e8/72/5d3c6d34562d019ba7f3102b2a6d0c8e9e24ef39e70f09645c36a66765b7/ty-0.0.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8a065eb2959f141fe4adafc14d57463cfa34f6cc4844a4ed56b2dce1a53a419a", size = 9701682, upload-time = "2025-12-18T02:16:41.379Z" },
{ url = "https://files.pythonhosted.org/packages/ef/44/bda434f788b320c9550a48c549e4a8c507e3d8a6ccb04ba5bd098307ba1e/ty-0.0.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e7177421f830a493f98d22f86d940b5a38788866e6062f680881f19be35ba3bb", size = 10213714, upload-time = "2025-12-18T02:16:35.648Z" },
{ url = "https://files.pythonhosted.org/packages/53/a6/b76a787938026c3d209131e5773de32cf6fc41210e0dd97874aafa20f394/ty-0.0.3-py3-none-win32.whl", hash = "sha256:e3e590bf5f33cb118a53c6d5242eedf7924d45517a5ee676c7a16be3a1389d2f", size = 9160441, upload-time = "2025-12-18T02:16:43.404Z" },
{ url = "https://files.pythonhosted.org/packages/fe/db/da60eb8252768323aee0ce69a08b95011088c003f80204b12380fe562fd2/ty-0.0.3-py3-none-win_amd64.whl", hash = "sha256:5af25b1fed8a536ce8072a9ae6a70cd2b559aa5294d43f57071fbdcd31dd2b0e", size = 10034265, upload-time = "2025-12-18T02:16:47.602Z" },
{ url = "https://files.pythonhosted.org/packages/5f/9c/9045cebdfc394c6f8c1e73a99d3aeda1bc639aace392e8ff4d695f1fab73/ty-0.0.3-py3-none-win_arm64.whl", hash = "sha256:29078b3100351a8b37339771615f13b8e4a4ff52b344d33f774f8d1a665a0ca5", size = 9513095, upload-time = "2025-12-18T02:16:59.073Z" },
]
[[package]] [[package]]
name = "types-cffi" name = "types-cffi"
version = "1.17.0.20250915" version = "1.17.0.20250915"