diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 200727e5..9346c8bb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,19 +23,17 @@ lint: - "**/*.py" - "pyproject.toml" - ".pre-commit-config.yaml" - - "pyrightconfig.json" when: on_success - when: never typecheck: stage: typecheck script: - - uv run pyright + - uv run ty rules: - changes: - "**/*.py" - "pyproject.toml" - - "pyrightconfig.json" when: on_success - when: never diff --git a/engine/blog/models.py b/engine/blog/models.py index c09133d6..f4bfb069 100644 --- a/engine/blog/models.py +++ b/engine/blog/models.py @@ -99,7 +99,7 @@ class Post(NiceModel): verbose_name = _("post") verbose_name_plural = _("posts") - def save(self, **kwargs): + def save(self, *args, **kwargs): if self.file: raise ValueError( _("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" ) ) - super().save(**kwargs) + super().save(*args, **kwargs) class PostTag(NiceModel): diff --git a/engine/core/abstract.py b/engine/core/abstract.py index 40851d7d..cdab177a 100644 --- a/engine/core/abstract.py +++ b/engine/core/abstract.py @@ -31,13 +31,13 @@ class NiceModel(Model): def save( self, - *, + *args, force_insert: bool = False, force_update: bool = False, using: str | None = None, update_fields: Collection[str] | None = None, update_modified: bool = True, - ) -> None: + ) -> None: # ty:ignore[invalid-method-override] self.update_modified = update_modified return super().save( force_insert=force_insert, diff --git a/engine/core/admin.py b/engine/core/admin.py index 88ff2f36..4c15e694 100644 --- a/engine/core/admin.py +++ b/engine/core/admin.py @@ -1,5 +1,5 @@ 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 ConstanceAdmin as BaseConstanceAdmin @@ -145,6 +145,7 @@ class FieldsetsMixin: # noinspection PyUnresolvedReferences class ActivationActionsMixin: + message_user: Callable actions_on_top = True actions_on_bottom = True actions = [ @@ -1086,10 +1087,8 @@ class ConstanceConfig: _meta = Meta() -# noinspection PyTypeChecker -site.unregister([Config]) -# noinspection PyTypeChecker -site.register([ConstanceConfig], BaseConstanceAdmin) +site.unregister([Config]) # ty:ignore[invalid-argument-type] +site.register([ConstanceConfig], BaseConstanceAdmin) # ty:ignore[invalid-argument-type] site.site_title = settings.PROJECT_NAME site.site_header = "eVibes" site.index_title = settings.PROJECT_NAME diff --git a/engine/core/elasticsearch/__init__.py b/engine/core/elasticsearch/__init__.py index 185ec46b..ccf39b11 100644 --- a/engine/core/elasticsearch/__init__.py +++ b/engine/core/elasticsearch/__init__.py @@ -10,6 +10,7 @@ from django_elasticsearch_dsl import fields from django_elasticsearch_dsl.registries import registry from elasticsearch import NotFoundError from elasticsearch.dsl import Q, Search +from elasticsearch.dsl.types import Hit from rest_framework.request import Request from engine.core.models import Brand, Category, Product @@ -199,7 +200,7 @@ def process_query( minimum_should_match=1, ) - def build_search(idxs: list[str], size: int) -> Search: + def build_search(idxs: list[str], size: int) -> Search[Hit]: return ( Search(index=idxs) .query(query_base) diff --git a/engine/core/filters.py b/engine/core/filters.py index 70fa2ef1..b8696e18 100644 --- a/engine/core/filters.py +++ b/engine/core/filters.py @@ -152,7 +152,7 @@ class ProductFilter(FilterSet): data: dict[Any, Any] | None = None, queryset: QuerySet[Product] | None = None, *, - request: HttpRequest | Request | Context = None, + request: HttpRequest | Request | Context | None = None, prefix: str | None = None, ) -> None: super().__init__(data=data, queryset=queryset, request=request, prefix=prefix) @@ -516,12 +516,12 @@ class CategoryFilter(FilterSet): if not value: return queryset - uuids = [ - category.get("uuid") - for category in process_query(query=value, indexes=("categories",))[ - "categories" - ] - ] + s_result = process_query(query=value, indexes=("categories",)) + + if not s_result: + raise ValueError("Search is unprocessable") + + uuids = [category.get("uuid") for category in s_result.get("categories", [])] return queryset.filter(uuid__in=uuids) diff --git a/engine/core/graphene/__init__.py b/engine/core/graphene/__init__.py index c461af03..e69de29b 100644 --- a/engine/core/graphene/__init__.py +++ b/engine/core/graphene/__init__.py @@ -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 diff --git a/engine/core/graphene/dashboard_mutations/__init__.py b/engine/core/graphene/dashboard_mutations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/engine/core/graphene/dashboard_mutations/product.py b/engine/core/graphene/dashboard_mutations/product.py deleted file mode 100644 index 747158dc..00000000 --- a/engine/core/graphene/dashboard_mutations/product.py +++ /dev/null @@ -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) diff --git a/engine/core/graphene/mutations.py b/engine/core/graphene/mutations.py index c66ebc98..d32a61ce 100644 --- a/engine/core/graphene/mutations.py +++ b/engine/core/graphene/mutations.py @@ -6,11 +6,10 @@ from django.core.cache import cache from django.core.exceptions import BadRequest, PermissionDenied from django.http import Http404 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 engine.core.elasticsearch import process_query -from engine.core.graphene import BaseMutation from engine.core.graphene.object_types import ( AddressType, BulkProductInput, @@ -32,7 +31,7 @@ logger = logging.getLogger(__name__) # noinspection PyUnusedLocal -class CacheOperator(BaseMutation): +class CacheOperator(Mutation): class Meta: description = _("cache I/O") @@ -54,7 +53,7 @@ class CacheOperator(BaseMutation): # noinspection PyUnusedLocal -class RequestCursedURL(BaseMutation): +class RequestCursedURL(Mutation): class Meta: description = _("request a CORSed URL") @@ -82,7 +81,7 @@ class RequestCursedURL(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class AddOrderProduct(BaseMutation): +class AddOrderProduct(Mutation): class Meta: description = _("add a product to the order") @@ -111,7 +110,7 @@ class AddOrderProduct(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class RemoveOrderProduct(BaseMutation): +class RemoveOrderProduct(Mutation): class Meta: description = _("remove a product from the order") @@ -140,7 +139,7 @@ class RemoveOrderProduct(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class RemoveAllOrderProducts(BaseMutation): +class RemoveAllOrderProducts(Mutation): class Meta: description = _("remove all products from the order") @@ -162,7 +161,7 @@ class RemoveAllOrderProducts(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class RemoveOrderProductsOfAKind(BaseMutation): +class RemoveOrderProductsOfAKind(Mutation): class Meta: description = _("remove a product from the order") @@ -185,7 +184,7 @@ class RemoveOrderProductsOfAKind(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class BuyOrder(BaseMutation): +class BuyOrder(Mutation): class Meta: description = _("buy an order") @@ -256,7 +255,7 @@ class BuyOrder(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class BulkOrderAction(BaseMutation): +class BulkOrderAction(Mutation): class Meta: description = _("perform an action on a list of products in the order") @@ -308,7 +307,7 @@ class BulkOrderAction(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class BulkWishlistAction(BaseMutation): +class BulkWishlistAction(Mutation): class Meta: description = _("perform an action on a list of products in the wishlist") @@ -349,7 +348,7 @@ class BulkWishlistAction(BaseMutation): # noinspection PyUnusedLocal -class BuyUnregisteredOrder(BaseMutation): +class BuyUnregisteredOrder(Mutation): class Meta: description = _("purchase an order without account creation") @@ -397,7 +396,7 @@ class BuyUnregisteredOrder(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class AddWishlistProduct(BaseMutation): +class AddWishlistProduct(Mutation): class Meta: description = _("add a product to the wishlist") @@ -425,7 +424,7 @@ class AddWishlistProduct(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class RemoveWishlistProduct(BaseMutation): +class RemoveWishlistProduct(Mutation): class Meta: description = _("remove a product from the wishlist") @@ -453,7 +452,7 @@ class RemoveWishlistProduct(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class RemoveAllWishlistProducts(BaseMutation): +class RemoveAllWishlistProducts(Mutation): class Meta: description = _("remove all products from the wishlist") @@ -481,7 +480,7 @@ class RemoveAllWishlistProducts(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class BuyWishlist(BaseMutation): +class BuyWishlist(Mutation): class Meta: description = _("buy all products from the wishlist") @@ -531,7 +530,7 @@ class BuyWishlist(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class BuyProduct(BaseMutation): +class BuyProduct(Mutation): class Meta: description = _("buy a product") @@ -576,7 +575,7 @@ class BuyProduct(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class FeedbackProductAction(BaseMutation): +class FeedbackProductAction(Mutation): class Meta: description = _("add or delete a feedback for orderproduct") @@ -611,7 +610,7 @@ class FeedbackProductAction(BaseMutation): # noinspection PyUnusedLocal,PyTypeChecker -class CreateAddress(BaseMutation): +class CreateAddress(Mutation): class Arguments: raw_data = String( required=True, description=_("original address string provided by the user") @@ -628,7 +627,7 @@ class CreateAddress(BaseMutation): # noinspection PyUnusedLocal -class DeleteAddress(BaseMutation): +class DeleteAddress(Mutation): class Arguments: uuid = UUID(required=True) @@ -655,7 +654,7 @@ class DeleteAddress(BaseMutation): # noinspection PyUnusedLocal -class AutocompleteAddress(BaseMutation): +class AutocompleteAddress(Mutation): class Arguments: q = String() limit = Int() @@ -676,7 +675,7 @@ class AutocompleteAddress(BaseMutation): # noinspection PyUnusedLocal -class ContactUs(BaseMutation): +class ContactUs(Mutation): class Arguments: email = String(required=True) name = String(required=True) @@ -707,7 +706,7 @@ class ContactUs(BaseMutation): # noinspection PyArgumentList PyUnusedLocal -class Search(BaseMutation): +class Search(Mutation): class Arguments: query = String(required=True) diff --git a/engine/core/models.py b/engine/core/models.py index 91d6b0cd..fd23abff 100644 --- a/engine/core/models.py +++ b/engine/core/models.py @@ -79,9 +79,6 @@ class AttributeGroup(ExportModelOperationsMixin("attribute_group"), NiceModel): ) is_publicly_visible = True - attributes: QuerySet["Attribute"] - children: QuerySet["Self"] - parent = ForeignKey( "self", on_delete=CASCADE, @@ -164,15 +161,7 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel): def __str__(self) -> str: return self.name - def save( - 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: + def save(self, *args, **kwargs) -> None: users = self.users.filter(is_active=True) users = users.exclude(attributes__icontains="is_business") if users.count() > 0: @@ -182,11 +171,11 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel): user.attributes.update({"is_business": True}) user.save() return super().save( - force_insert=force_insert, - force_update=force_update, - using=using, - update_fields=update_fields, - update_modified=update_modified, + force_insert=kwargs.get("force_insert", False), + force_update=kwargs.get("force_update", False), + using=kwargs.get("using"), + update_fields=kwargs.get("update_fields"), + update_modified=kwargs.get("update_modified", True), ) class Meta: @@ -704,7 +693,9 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): @cached_property 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 def rating(self) -> float: diff --git a/engine/core/views.py b/engine/core/views.py index e7622e32..03a8e524 100644 --- a/engine/core/views.py +++ b/engine/core/views.py @@ -4,7 +4,6 @@ import os import traceback from contextlib import suppress from datetime import date, timedelta -from typing import Any import requests from constance import config @@ -16,7 +15,6 @@ from django.core.exceptions import BadRequest from django.db.models import Count, F, Sum from django.http import ( FileResponse, - Http404, HttpRequest, HttpResponse, 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.translation import gettext_lazy as _ 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_ratelimit.decorators import ratelimit from drf_spectacular.utils import extend_schema_view @@ -404,14 +403,13 @@ class DownloadDigitalAssetView(APIView): ) -def favicon_view( - request: HttpRequest, *args: list[Any], **kwargs: dict[str, Any] -) -> HttpResponse | FileResponse | None: +@csrf_exempt +def favicon_view(request: HttpRequest) -> HttpResponse | FileResponse: try: favicon_path = os.path.join(settings.BASE_DIR, "static/favicon.png") return FileResponse(open(favicon_path, "rb"), content_type="image/x-icon") - except FileNotFoundError as fnfe: - raise Http404(_("favicon not found")) from fnfe + except FileNotFoundError: + return HttpResponse(status=404, content=_("favicon not found")) # noinspection PyTypeChecker diff --git a/engine/payments/graphene/mutations.py b/engine/payments/graphene/mutations.py index 42d71fb7..85873460 100644 --- a/engine/payments/graphene/mutations.py +++ b/engine/payments/graphene/mutations.py @@ -1,13 +1,13 @@ import graphene +from graphene import Mutation from rest_framework.exceptions import PermissionDenied -from engine.core.graphene import BaseMutation from engine.core.utils.messages import permission_denied_message from engine.payments.graphene.object_types import TransactionType from engine.payments.models import Transaction -class Deposit(BaseMutation): +class Deposit(Mutation): class Arguments: amount = graphene.Float(required=True) diff --git a/engine/vibes_auth/graphene/mutations.py b/engine/vibes_auth/graphene/mutations.py index 9b914c71..04a2e1a3 100644 --- a/engine/vibes_auth/graphene/mutations.py +++ b/engine/vibes_auth/graphene/mutations.py @@ -11,11 +11,10 @@ from django.db import IntegrityError from django.http import Http404 from django.utils.http import urlsafe_base64_decode 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_file_upload.scalars import Upload -from engine.core.graphene import BaseMutation from engine.core.utils.messages import permission_denied_message from engine.core.utils.security import is_safe_key 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__) -class CreateUser(BaseMutation): +class CreateUser(Mutation): class Arguments: email = String(required=True) password = String(required=True) @@ -92,7 +91,7 @@ class CreateUser(BaseMutation): raise BadRequest(str(e)) from e -class UpdateUser(BaseMutation): +class UpdateUser(Mutation): class Arguments: uuid = UUID(required=True) email = String(required=False) @@ -187,7 +186,7 @@ class UpdateUser(BaseMutation): raise BadRequest(str(e)) from e -class DeleteUser(BaseMutation): +class DeleteUser(Mutation): class Arguments: email = String() uuid = UUID() @@ -212,7 +211,7 @@ class DeleteUser(BaseMutation): raise PermissionDenied(permission_denied_message) -class ObtainJSONWebToken(BaseMutation): +class ObtainJSONWebToken(Mutation): class Arguments: email = String(required=True) password = String(required=True) @@ -236,7 +235,7 @@ class ObtainJSONWebToken(BaseMutation): raise PermissionDenied(f"invalid credentials provided: {e!s}") from e -class RefreshJSONWebToken(BaseMutation): +class RefreshJSONWebToken(Mutation): class Arguments: refresh_token = String(required=True) @@ -259,7 +258,7 @@ class RefreshJSONWebToken(BaseMutation): raise PermissionDenied(f"invalid refresh token provided: {e!s}") from e -class VerifyJSONWebToken(BaseMutation): +class VerifyJSONWebToken(Mutation): class Arguments: token = String(required=True) @@ -281,7 +280,7 @@ class VerifyJSONWebToken(BaseMutation): return VerifyJSONWebToken(token_is_valid=False, user=None, detail=detail) -class ActivateUser(BaseMutation): +class ActivateUser(Mutation): class Arguments: uid = String(required=True) token = String(required=True) @@ -311,7 +310,7 @@ class ActivateUser(BaseMutation): return ActivateUser(success=True) -class ResetPassword(BaseMutation): +class ResetPassword(Mutation): class Arguments: email = String(required=True) @@ -330,7 +329,7 @@ class ResetPassword(BaseMutation): return ResetPassword(success=True) -class ConfirmResetPassword(BaseMutation): +class ConfirmResetPassword(Mutation): class Arguments: uid = String(required=True) token = String(required=True) @@ -370,7 +369,7 @@ class ConfirmResetPassword(BaseMutation): raise BadRequest(_(f"something went wrong: {e!s}")) from e -class UploadAvatar(BaseMutation): +class UploadAvatar(Mutation): class Arguments: file = Upload(required=True) diff --git a/engine/vibes_auth/serializers.py b/engine/vibes_auth/serializers.py index 9876ce65..4f865a2c 100644 --- a/engine/vibes_auth/serializers.py +++ b/engine/vibes_auth/serializers.py @@ -233,7 +233,7 @@ class TokenRefreshSerializer(Serializer): if api_settings.ROTATE_REFRESH_TOKENS: if api_settings.BLACKLIST_AFTER_ROTATION: with suppress(AttributeError): - refresh.blacklist() + refresh.blacklist() # ty:ignore[possibly-missing-attribute] refresh.set_jti() refresh.set_exp() diff --git a/engine/vibes_auth/viewsets.py b/engine/vibes_auth/viewsets.py index c0aa7d45..9942a514 100644 --- a/engine/vibes_auth/viewsets.py +++ b/engine/vibes_auth/viewsets.py @@ -2,7 +2,6 @@ import logging import traceback from contextlib import suppress from secrets import compare_digest -from typing import Type from django.conf import settings from django.contrib.auth.password_validation import validate_password @@ -53,7 +52,7 @@ class UserViewSet( queryset = User.objects.filter(is_active=True) permission_classes = [AllowAny] - @action(detail=False, methods=["post"]) + @action(detail=False, methods=("POST",)) @method_decorator( 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)) 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( 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) - @action(detail=False, methods=["post"]) + @action(detail=False, methods=("POST",)) @method_decorator( ratelimit(key="ip", rate="5/h" if not settings.DEBUG else "888/h") ) def confirm_password_reset(self, request: Request, *args, **kwargs) -> Response: try: 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( {"error": _("passwords do not match")}, 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) - validate_password(password=request.data.get("password"), user=user) + validate_password(password=str(request.data.get("password")), user=user) password_reset_token = PasswordResetTokenGenerator() 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 ) - @action(detail=False, methods=["post"]) + @action(detail=False, methods=("POST",)) @method_decorator( ratelimit(key="ip", rate="5/h" if not settings.DEBUG else "888/h") ) def activate(self, request: Request) -> Response: detail = "" - activation_error: Type[Exception] | None = None + activation_error: Exception | None = None 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) if not user.check_token( - urlsafe_base64_decode(request.data.get("token")).decode() + urlsafe_base64_decode(str(request.data.get("token"))).decode() ): return Response( {"error": _("activation link is invalid!")}, @@ -175,7 +175,7 @@ class UserViewSet( detail = str(traceback.format_exc()) if user is None: if settings.DEBUG: - raise Exception from activation_error + raise Exception from activation_error # ty:ignore[possibly-unresolved-reference] return Response( {"error": _("activation link is invalid!"), "detail": detail}, status=status.HTTP_400_BAD_REQUEST, @@ -187,7 +187,7 @@ class UserViewSet( response_data["access"] = str(tokens.access_token) 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: user = self.get_object() if request.user != user: diff --git a/evibes/ftpstorage.py b/evibes/ftpstorage.py deleted file mode 100644 index 54b16ebf..00000000 --- a/evibes/ftpstorage.py +++ /dev/null @@ -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 diff --git a/evibes/middleware.py b/evibes/middleware.py index ab8b81cc..6cc88264 100644 --- a/evibes/middleware.py +++ b/evibes/middleware.py @@ -17,6 +17,7 @@ from django.http import ( HttpResponseForbidden, HttpResponsePermanentRedirect, JsonResponse, + QueryDict, ) from django.middleware.common import CommonMiddleware from django.middleware.locale import LocaleMiddleware @@ -177,10 +178,21 @@ class CamelCaseMiddleWare: self.get_response = get_response def __call__(self, request): - request.GET = underscoreize( - request.GET, + underscoreized_get = underscoreize( + {k: v for k, v in request.GET.lists()}, **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) return response diff --git a/evibes/pagination.py b/evibes/pagination.py index ad0376cb..bdda17f3 100644 --- a/evibes/pagination.py +++ b/evibes/pagination.py @@ -10,6 +10,8 @@ class CustomPagination(PageNumberPagination): ) def get_paginated_response(self, data: dict[str, Any]) -> Response: + if not self.page: + raise RuntimeError return Response( { "links": { @@ -25,9 +27,7 @@ class CustomPagination(PageNumberPagination): } ) - def get_paginated_response_schema( - self, data_schema: dict[str, Any] - ) -> dict[str, Any]: + def get_paginated_response_schema(self, schema: dict[str, Any]) -> dict[str, Any]: return { "type": "object", "properties": { @@ -63,6 +63,6 @@ class CustomPagination(PageNumberPagination): "example": 100, "description": "Total number of items", }, - "data": data_schema, + "data": schema, }, } diff --git a/evibes/settings/base.py b/evibes/settings/base.py index 39f67cbf..34980f64 100644 --- a/evibes/settings/base.py +++ b/evibes/settings/base.py @@ -348,7 +348,7 @@ LANGUAGE_URL_OVERRIDES: dict[str, str] = { 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") @@ -517,7 +517,7 @@ if getenv("DBBACKUP_HOST") and getenv("DBBACKUP_USER") and getenv("DBBACKUP_PASS STORAGES.update( { "dbbackup": { - "BACKEND": "evibes.ftpstorage.AbsoluteFTPStorage", + "BACKEND": "storages.backends.ftp.FTPStorage", "OPTIONS": { "location": ( f"ftp://{getenv('DBBACKUP_USER')}:{getenv('DBBACKUP_PASS')}@{getenv('DBBACKUP_HOST')}:21/{raw_path}" diff --git a/evibes/settings/drf.py b/evibes/settings/drf.py index c342a153..37473c48 100644 --- a/evibes/settings/drf.py +++ b/evibes/settings/drf.py @@ -1,5 +1,6 @@ from datetime import timedelta from os import getenv +from typing import Any from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ @@ -12,7 +13,7 @@ from evibes.settings.base import ( 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", "PAGE_SIZE": 30, "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] = { - "ACCESS_TOKEN_LIFETIME": timedelta(hours=8) if not DEBUG else timedelta(hours=88), - "REFRESH_TOKEN_LIFETIME": timedelta(days=8), + "ACCESS_TOKEN_LIFETIME": access_lifetime, + "REFRESH_TOKEN_LIFETIME": refresh_lifetime, "ROTATE_REFRESH_TOKENS": True, "BLACKLIST_AFTER_ROTATION": True, "UPDATE_LAST_LOGIN": True, @@ -90,15 +94,14 @@ The API supports multiple response formats: Current API version: %(version)s """) # noqa: E501, F405 -_access_seconds = SIMPLE_JWT.get("ACCESS_TOKEN_LIFETIME").total_seconds() if not DEBUG: - _access_lifetime = int(_access_seconds // 60) + _access_lifetime = int(access_lifetime.total_seconds() // 60) _access_unit = "minutes" else: - _access_lifetime = int(_access_seconds // 3600) + _access_lifetime = int(access_lifetime.total_seconds() // 3600) _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_TEMPLATE, diff --git a/evibes/signal_processors.py b/evibes/signal_processors.py index c35f0f38..6ef0473c 100644 --- a/evibes/signal_processors.py +++ b/evibes/signal_processors.py @@ -1,6 +1,8 @@ from django.db import models 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): diff --git a/evibes/urls.py b/evibes/urls.py index 4edd993a..50ee7969 100644 --- a/evibes/urls.py +++ b/evibes/urls.py @@ -48,6 +48,7 @@ urlpatterns = [ path( r"favicon.ico", favicon_view, + name="favicon", ), path( r"graphql/", diff --git a/evibes/utils/renderers.py b/evibes/utils/renderers.py index d68ec38b..af26e05b 100644 --- a/evibes/utils/renderers.py +++ b/evibes/utils/renderers.py @@ -1,5 +1,5 @@ import re -from typing import Any, MutableMapping +from typing import Any, Collection, MutableMapping from django.utils.module_loading import import_string from drf_orjson_renderer.renderers import ORJSONRenderer @@ -43,12 +43,12 @@ def camelize(obj: Any) -> Any: def camelize_serializer_fields(result, generator, request, public): - ignore_fields = JSON_UNDERSCOREIZE.get("ignore_fields") or () - ignore_keys = JSON_UNDERSCOREIZE.get("ignore_keys") or () + ignore_fields: Collection[Any] = JSON_UNDERSCOREIZE.get("ignore_fields", ()) + ignore_keys: Collection[Any] = JSON_UNDERSCOREIZE.get("ignore_keys", ()) def has_middleware_installed(): try: - from djangorestframework_camel_case.middleware import CamelCaseMiddleWare + from evibes.middleware import CamelCaseMiddleWare except ImportError: return False @@ -104,7 +104,7 @@ def camelize_serializer_fields(result, generator, request, public): class CamelCaseRenderer(ORJSONRenderer): def render( self, data: Any, media_type: str | None = None, renderer_context: Any = None - ) -> bytes: + ) -> bytes: # ty:ignore[invalid-method-override] if data is None: return b"" diff --git a/pyproject.toml b/pyproject.toml index f2002a12..28459778 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,9 +86,8 @@ worker = [ "celery-prometheus-exporter==1.7.0", ] linting = [ + "ty==0.0.3", "ruff==0.14.9", - "basedpyright>=1.36.1", - "pyright==1.1.407", "celery-types>=0.23.0", "django-stubs==5.2.8", "djangorestframework-stubs==3.16.6", @@ -112,7 +111,15 @@ python-version = "3.13" [tool.ruff] line-length = 88 target-version = "py312" -exclude = ["media", "static", "storefront"] +exclude = [ + "Dockerfiles", + "monitoring", + "scripts", + "static", + "storefront", + "tmp", + "media", +] [tool.ruff.lint] select = ["E4", "E7", "E9", "F", "B", "Q", "I"] @@ -125,41 +132,25 @@ known-first-party = ["evibes", "engine"] quote-style = "double" indent-style = "space" -[tool.pyright] -typeCheckingMode = "strict" -pythonVersion = "3.12" -useLibraryCodeForTypes = true -reportMissingTypeStubs = true -reportGeneralTypeIssues = false -reportRedeclaration = false -exclude = [ - "**/__pycache__/**", - "**/.venv/**", - "**/.uv/**", - "media/**", - "static/**", - "storefront/**", - "**/migrations/**", -] -extraPaths = ["./evibes", "./engine"] +[tool.ty.environment] +python-version = "3.12" -[tool.basedpyright] -typeCheckingMode = "strict" -pythonVersion = "3.12" -useLibraryCodeForTypes = true -reportMissingTypeStubs = true -reportGeneralTypeIssues = false -reportRedeclaration = false +[tool.ty.terminal] +output-format = "concise" + +[tool.ty.rules] +possibly-unresolved-reference = "warn" + +[[tool.ty.overrides]] exclude = [ - "**/__pycache__/**", - "**/.venv/**", - "**/.uv/**", - "media/**", + "Dockerfiles/**", + "monitoring/**", + "scripts/**", "static/**", "storefront/**", - "**/migrations/**", + "tmp/**", + "media/**", ] -extraPaths = ["./evibes", "./engine"] [tool.django-stubs] django_settings_module = "evibes.settings.__init__" diff --git a/uv.lock b/uv.lock index bc306205..5c6f470e 100644 --- a/uv.lock +++ b/uv.lock @@ -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" }, ] -[[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]] name = "bcrypt" version = "5.0.0" @@ -1420,12 +1408,11 @@ jupyter = [ { name = "jupyter" }, ] linting = [ - { name = "basedpyright" }, { name = "celery-types" }, { name = "django-stubs" }, { name = "djangorestframework-stubs" }, - { name = "pyright" }, { name = "ruff" }, + { name = "ty" }, { name = "types-docutils" }, { name = "types-paramiko" }, { name = "types-pillow" }, @@ -1448,7 +1435,6 @@ worker = [ requires-dist = [ { name = "aiogram", specifier = "==3.23.0" }, { 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-prometheus-exporter", marker = "extra == 'worker'", specifier = "==1.7.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 = "pyjwt", specifier = "==2.10.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-django", specifier = "==4.11.1" }, { 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 = "six", specifier = "==1.17.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-paramiko", marker = "extra == 'linting'", specifier = "==4.0.0.20250822" }, { 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" }, ] -[[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]] name = "notebook" 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" }, ] -[[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]] name = "pytest" 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" }, ] +[[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]] name = "types-cffi" version = "1.17.0.20250915"