diff --git a/engine/core/graphene/object_types.py b/engine/core/graphene/object_types.py index bc2b323a..cd0c9e0a 100644 --- a/engine/core/graphene/object_types.py +++ b/engine/core/graphene/object_types.py @@ -3,8 +3,7 @@ from contextlib import suppress from typing import Any from django.conf import settings -from django.core.cache import cache -from django.db.models import Max, Min, QuerySet +from django.db.models import QuerySet from django.utils.translation import gettext_lazy as _ from graphene import ( UUID, @@ -279,21 +278,10 @@ class CategoryType(DjangoObjectType): return self.filterable_attributes def resolve_min_max_prices(self: Category, _info): - min_max_prices = cache.get(key=f"{self.name}_min_max_prices", default={}) - - if not min_max_prices: - price_aggregation = Product.objects.filter(category=self).aggregate( - min_price=Min("stocks__price"), max_price=Max("stocks__price") - ) - min_max_prices["min_price"] = price_aggregation.get("min_price", 0.0) - min_max_prices["max_price"] = price_aggregation.get("max_price", 0.0) - cache.set( - key=f"{self.name}_min_max_prices", value=min_max_prices, timeout=86400 - ) return { - "min_price": min_max_prices["min_price"], - "max_price": min_max_prices["max_price"], + "min_price": self.min_price, + "max_price": self.max_price, } def resolve_brands(self: Category, info) -> QuerySet[Brand]: diff --git a/engine/core/management/commands/clear_unwanted.py b/engine/core/management/commands/clear_unwanted.py index c2ed53a8..d37853f9 100644 --- a/engine/core/management/commands/clear_unwanted.py +++ b/engine/core/management/commands/clear_unwanted.py @@ -14,7 +14,7 @@ class Command(BaseCommand): # Group stocks by (product, vendor) stocks_by_group = defaultdict(list) for stock in Stock.objects.all().order_by("modified"): - stocks_by_group[stock.product_pk].append(stock) + stocks_by_group[stock.product_pk].append(stock) # ty: ignore[possibly-missing-attribute] stock_deletions: list[str] = [] for group in stocks_by_group.values(): diff --git a/engine/core/management/commands/demo_data.py b/engine/core/management/commands/demo_data.py index 6ab5c031..6699fcba 100644 --- a/engine/core/management/commands/demo_data.py +++ b/engine/core/management/commands/demo_data.py @@ -386,9 +386,9 @@ class Command(BaseCommand): if created: if "name_ru" in prod_data: - product.name_ru_ru = prod_data["name_ru"] + product.name_ru_ru = prod_data["name_ru"] # ty: ignore[invalid-assignment] if "description_ru" in prod_data: - product.description_ru_ru = prod_data["description_ru"] + product.description_ru_ru = prod_data["description_ru"] # ty: ignore[invalid-assignment] product.save() Stock.objects.create( diff --git a/engine/core/models.py b/engine/core/models.py index 0bea4ec4..0add9262 100644 --- a/engine/core/models.py +++ b/engine/core/models.py @@ -2,7 +2,7 @@ import datetime import json import logging from contextlib import suppress -from typing import Any, Iterable, Self +from typing import TYPE_CHECKING, Any, Iterable, Self from constance import config from django.conf import settings @@ -29,6 +29,7 @@ from django.db.models import ( JSONField, ManyToManyField, Max, + Min, OneToOneField, PositiveIntegerField, QuerySet, @@ -72,6 +73,9 @@ from engine.core.validators import validate_category_image_dimensions from engine.payments.models import Transaction from schon.utils.misc import create_object +if TYPE_CHECKING: + from django.db.models import Manager + logger = logging.getLogger(__name__) @@ -83,6 +87,9 @@ class AttributeGroup(NiceModel): " This can be useful for categorizing and managing attributes more effectively in acomplex system." ) + if TYPE_CHECKING: + attributes: Manager["Attribute"] + is_publicly_visible = True parent = ForeignKey( "self", @@ -266,6 +273,10 @@ class Category(NiceModel, MPTTModel): " as well as assign attributes like images, tags, or priority." ) + if TYPE_CHECKING: + products: Manager["Product"] + children: Manager["Category"] + is_publicly_visible = True image = ImageField( @@ -450,6 +461,20 @@ class Category(NiceModel, MPTTModel): is_active=True, ).distinct() + @cached_property + def min_price(self) -> float: + return ( + self.products.filter(is_active=True).aggregate(Min("price"))["price__min"] + or 0.0 + ) + + @cached_property + def max_price(self) -> float: + return ( + self.products.filter(is_active=True).aggregate(Max("price"))["price__max"] + or 0.0 + ) + class Meta: verbose_name = _("category") verbose_name_plural = _("categories") @@ -604,6 +629,12 @@ class Product(NiceModel): " its associated information within an application." ) + if TYPE_CHECKING: + images: Manager["ProductImage"] + stocks: Manager["Stock"] + attributes: Manager["AttributeValue"] + category_id: Any + is_publicly_visible = True category = ForeignKey( @@ -1274,6 +1305,10 @@ class Order(NiceModel): " Equally, functionality supports managing the products in the order lifecycle." ) + if TYPE_CHECKING: + order_products: Manager["OrderProduct"] + payments_transactions: Manager[Transaction] + is_publicly_visible = False billing_address = ForeignKey( @@ -1442,7 +1477,8 @@ class Order(NiceModel): if promotions.exists(): buy_price -= round( - product.price * (promotions.first().discount_percent / 100), 2 + product.price * (promotions.first().discount_percent / 100), # ty: ignore[possibly-missing-attribute] + 2, ) order_product, is_created = OrderProduct.objects.get_or_create( @@ -1487,7 +1523,7 @@ class Order(NiceModel): order_product.delete() return self if order_product.quantity == 1: - self.order_products.remove(order_product) + self.order_products.remove(order_product) # ty: ignore[unresolved-attribute] order_product.delete() else: order_product.quantity -= 1 @@ -1510,7 +1546,7 @@ class Order(NiceModel): _("you cannot remove products from an order that is not a pending one") ) for order_product in self.order_products.all(): - self.order_products.remove(order_product) + self.order_products.remove(order_product) # ty: ignore[unresolved-attribute] order_product.delete() return self @@ -1522,7 +1558,7 @@ class Order(NiceModel): try: product = Product.objects.get(uuid=product_uuid) order_product = self.order_products.get(product=product, order=self) - self.order_products.remove(order_product) + self.order_products.remove(order_product) # ty: ignore[unresolved-attribute] order_product.delete() except Product.DoesNotExist as dne: name = "Product" @@ -1788,6 +1824,8 @@ class Order(NiceModel): crm_links = OrderCrmLink.objects.filter(order=self) if crm_links.exists(): crm_link = crm_links.first() + if not crm_link: + return False crm_integration = create_object( crm_link.crm.integration_location, crm_link.crm.name ) @@ -1894,6 +1932,9 @@ class OrderProduct(NiceModel): "and stores a reference to them." ) + if TYPE_CHECKING: + download: "DigitalAssetDownload" + is_publicly_visible = False buy_price = FloatField( diff --git a/engine/core/utils/commerce.py b/engine/core/utils/commerce.py index c52254e1..c5ca430d 100644 --- a/engine/core/utils/commerce.py +++ b/engine/core/utils/commerce.py @@ -174,7 +174,7 @@ def get_top_returned_products( p = product_by_id[pid] img = "" with suppress(Exception): - img = p.images.first().image_url if p.images.exists() else "" + img = p.images.first().image_url if p.images.exists() else "" # ty: ignore[possibly-missing-attribute] result.append( { "name": p.name, diff --git a/engine/core/vendors/__init__.py b/engine/core/vendors/__init__.py index 563c78f1..f027c6a5 100644 --- a/engine/core/vendors/__init__.py +++ b/engine/core/vendors/__init__.py @@ -8,7 +8,7 @@ from datetime import datetime from decimal import Decimal from io import BytesIO from math import ceil, log10 -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from constance import config from django.conf import settings @@ -36,6 +36,8 @@ from engine.payments.errors import RatesError from engine.payments.utils import get_rates from schon.utils.misc import LoggingError, LogLevel +_BrandOrCategory = TypeVar("_BrandOrCategory", Brand, Category) + if TYPE_CHECKING: from engine.core.models import OrderProduct @@ -320,8 +322,8 @@ class AbstractVendor(ABC): @staticmethod def _auto_resolver_helper( - model: type[Brand] | type[Category], resolving_name: str - ) -> Brand | Category | None: + model: type[_BrandOrCategory], resolving_name: str + ) -> _BrandOrCategory | None: """Internal helper for resolving Brand/Category by name with deduplication.""" queryset = model.objects.filter(name=resolving_name) if not queryset.exists(): @@ -672,6 +674,8 @@ class AbstractVendor(ABC): .order_by("uuid") .first() ) + if not attribute: + return None fields_to_update: list[str] = [] if not attribute.is_active: attribute.is_active = True diff --git a/engine/core/views.py b/engine/core/views.py index 908acc41..73fa36f9 100644 --- a/engine/core/views.py +++ b/engine/core/views.py @@ -607,7 +607,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: product = Product.objects.filter(pk=wished_first["products"]).first() if product: img = ( - product.images.first().image_url if product.images.exists() else "" + product.images.first().image_url if product.images.exists() else "" # ty: ignore[possibly-missing-attribute] ) most_wished = { "name": product.name, @@ -631,7 +631,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: if not pid or pid not in product_by_id: continue p = product_by_id[pid] - img = p.images.first().image_url if p.images.exists() else "" + img = p.images.first().image_url if p.images.exists() else "" # ty: ignore[possibly-missing-attribute] most_wished_list.append( { "name": p.name, @@ -687,10 +687,10 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: .order_by("total_qty")[:5] ) for p in products: - qty = int(p.total_qty or 0) + qty = int(p.total_qty or 0) # ty: ignore[possibly-missing-attribute] img = "" with suppress(Exception): - img = p.images.first().image_url if p.images.exists() else "" + img = p.images.first().image_url if p.images.exists() else "" # ty: ignore[possibly-missing-attribute] low_stock_list.append( { "name": p.name, @@ -734,7 +734,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: product = Product.objects.filter(pk=popular_first["product"]).first() if product: img = ( - product.images.first().image_url if product.images.exists() else "" + product.images.first().image_url if product.images.exists() else "" # ty: ignore[possibly-missing-attribute] ) most_popular = { "name": product.name, @@ -758,7 +758,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: if not pid or pid not in product_by_id: continue p = product_by_id[pid] - img = p.images.first().image_url if p.images.exists() else "" + img = p.images.first().image_url if p.images.exists() else "" # ty: ignore[possibly-missing-attribute] most_popular_list.append( { "name": p.name, diff --git a/pyproject.toml b/pyproject.toml index f9e7b415..9e8e23df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "django-elasticsearch-dsl==8.2", "django-extensions==4.1", "django-filter==25.2", - "django-health-check==4.0.4", + "django-health-check==4.0.6", "django-import-export[all]==4.4.0", "django-json-widget==2.1.1", "django-model-utils==5.0.0", @@ -90,10 +90,10 @@ linting = [ "types-paramiko==4.0.0.20250822", "types-psutil==7.2.2.20260130", "types-pillow==10.2.0.20240822", - "types-docutils==0.22.3.20251115", + "types-docutils==0.22.3.20260223", "types-six==1.17.0.20251009", ] -openai = ["openai==2.21.0"] +openai = ["openai==2.24.0"] jupyter = ["jupyter==1.1.1"] [tool.uv] diff --git a/uv.lock b/uv.lock index c6464e18..48638237 100644 --- a/uv.lock +++ b/uv.lock @@ -388,11 +388,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.2.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] @@ -880,15 +880,15 @@ wheels = [ [[package]] name = "django-health-check" -version = "4.0.4" +version = "4.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, { name = "dnspython" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/ea/5abd492cc9ea536edba5d436a84086f1c0fcdc66fd023a1f4cc086d39a56/django_health_check-4.0.4.tar.gz", hash = "sha256:b2349ff9d75dc52e203be20f461eabae6b203f2566e5ba888bc885168decaaa9", size = 20496, upload-time = "2026-02-18T13:08:42.442Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/fe/718725c58fd177cff0cfb8abe3010f2cad582713f2bc52eaf7120b750dec/django_health_check-4.0.6.tar.gz", hash = "sha256:03837041ba8a235e810e16218f2ef3feb372c4af72776fa3676c16435c72171c", size = 20763, upload-time = "2026-02-23T17:11:40.625Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/12/8f1fc3b2bd8516c4e71d988b1218543ab0ef3fd21545302bdaf91a57f50d/django_health_check-4.0.4-py3-none-any.whl", hash = "sha256:6c91efa2e3b4f4b280aa5646b6347385f57010314c395aa6af3f7c64f75cd1f8", size = 25476, upload-time = "2026-02-18T13:08:40.91Z" }, + { url = "https://files.pythonhosted.org/packages/3a/44/2fa6ec47c1c18159c094f7d00397a208b6311e8b26d603dd22ba6e79b99d/django_health_check-4.0.6-py3-none-any.whl", hash = "sha256:efba106bc4f92b1b084f3af751e9eeb0b5c1af77d0af212e432ede2ba8f1e94f", size = 25813, upload-time = "2026-02-23T17:11:39.419Z" }, ] [[package]] @@ -1920,7 +1920,7 @@ wheels = [ [[package]] name = "jupyterlab" -version = "4.5.4" +version = "4.5.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, @@ -1937,9 +1937,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/6b/21af7c0512bdf67e0c54c121779a1f2a97a164a7657e13fced79db8fa5a0/jupyterlab-4.5.4.tar.gz", hash = "sha256:c215f48d8e4582bd2920ad61cc6a40d8ebfef7e5a517ae56b8a9413c9789fdfb", size = 23943597, upload-time = "2026-02-11T00:26:55.308Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/2d/953a5612a34a3c799a62566a548e711d103f631672fd49650e0f2de80870/jupyterlab-4.5.5.tar.gz", hash = "sha256:eac620698c59eb810e1729909be418d9373d18137cac66637141abba613b3fda", size = 23968441, upload-time = "2026-02-23T18:57:34.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/9f/a70972ece62ead2d81acc6223188f6d18a92f665ccce17796a0cdea4fcf5/jupyterlab-4.5.4-py3-none-any.whl", hash = "sha256:cc233f70539728534669fb0015331f2a3a87656207b3bb2d07916e9289192f12", size = 12391867, upload-time = "2026-02-11T00:26:51.23Z" }, + { url = "https://files.pythonhosted.org/packages/b9/52/372d3494766d690dfdd286871bf5f7fb9a6c61f7566ccaa7153a163dd1df/jupyterlab-4.5.5-py3-none-any.whl", hash = "sha256:a35694a40a8e7f2e82f387472af24e61b22adcce87b5a8ab97a5d9c486202a6d", size = 12446824, upload-time = "2026-02-23T18:57:30.398Z" }, ] [[package]] @@ -2281,7 +2281,7 @@ wheels = [ [[package]] name = "notebook" -version = "7.5.3" +version = "7.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, @@ -2290,9 +2290,9 @@ dependencies = [ { name = "notebook-shim" }, { name = "tornado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b8/cb/cc7f4df5cee315dd126a47eb60890690a0438d5e0dd40c32d60ce16de377/notebook-7.5.3.tar.gz", hash = "sha256:393ceb269cf9fdb02a3be607a57d7bd5c2c14604f1818a17dbeb38e04f98cbfa", size = 14073140, upload-time = "2026-01-26T07:28:36.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/08/9d446fbb49f95de316ea6d7f25d0a4bc95117dd574e35f405895ac706f29/notebook-7.5.4.tar.gz", hash = "sha256:b928b2ba22cb63aa83df2e0e76fe3697950a0c1c4a41b84ebccf1972b1bb5771", size = 14167892, upload-time = "2026-02-24T14:13:56.116Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/98/9286e7f35e5584ebb79f997f2fb0cb66745c86f6c5fccf15ba32aac5e908/notebook-7.5.3-py3-none-any.whl", hash = "sha256:c997bfa1a2a9eb58c9bbb7e77d50428befb1033dd6f02c482922e96851d67354", size = 14481744, upload-time = "2026-01-26T07:28:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/59/01/05e5387b53e0f549212d5eff58845886f3827617b5c9409c966ddc07cb6d/notebook-7.5.4-py3-none-any.whl", hash = "sha256:860e31782b3d3a25ca0819ff039f5cf77845d1bf30c78ef9528b88b25e0a9850", size = 14578014, upload-time = "2026-02-24T14:13:52.274Z" }, ] [[package]] @@ -2358,7 +2358,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/97/73/8ade73f6749177003 [[package]] name = "openai" -version = "2.21.0" +version = "2.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2370,9 +2370,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/e5/3d197a0947a166649f566706d7a4c8f7fe38f1fa7b24c9bcffe4c7591d44/openai-2.21.0.tar.gz", hash = "sha256:81b48ce4b8bbb2cc3af02047ceb19561f7b1dc0d4e52d1de7f02abfd15aa59b7", size = 644374, upload-time = "2026-02-14T00:12:01.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/56/0a89092a453bb2c676d66abee44f863e742b2110d4dbb1dbcca3f7e5fc33/openai-2.21.0-py3-none-any.whl", hash = "sha256:0bc1c775e5b1536c294eded39ee08f8407656537ccc71b1004104fe1602e267c", size = 1103065, upload-time = "2026-02-14T00:11:59.603Z" }, + { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, ] [[package]] @@ -3440,7 +3440,7 @@ requires-dist = [ { name = "django-elasticsearch-dsl", specifier = "==8.2" }, { name = "django-extensions", specifier = "==4.1" }, { name = "django-filter", specifier = "==25.2" }, - { name = "django-health-check", specifier = "==4.0.4" }, + { name = "django-health-check", specifier = "==4.0.6" }, { name = "django-import-export", extras = ["all"], specifier = "==4.4.0" }, { name = "django-json-widget", specifier = "==2.1.1" }, { name = "django-md-field", specifier = "==0.1.0" }, @@ -3471,7 +3471,7 @@ requires-dist = [ { name = "graphene-file-upload", specifier = "==1.3.0" }, { name = "httpx", specifier = "==0.28.1" }, { name = "jupyter", marker = "extra == 'jupyter'", specifier = "==1.1.1" }, - { name = "openai", marker = "extra == 'openai'", specifier = "==2.21.0" }, + { name = "openai", marker = "extra == 'openai'", specifier = "==2.24.0" }, { name = "opentelemetry-instrumentation-django", specifier = "==0.60b1" }, { name = "paramiko", specifier = "==4.0.0" }, { name = "pillow", specifier = "==12.1.1" }, @@ -3490,7 +3490,7 @@ requires-dist = [ { name = "six", specifier = "==1.17.0" }, { name = "swapper", specifier = "==1.4.0" }, { name = "ty", marker = "extra == 'linting'", specifier = "==0.0.16" }, - { name = "types-docutils", marker = "extra == 'linting'", specifier = "==0.22.3.20251115" }, + { name = "types-docutils", marker = "extra == 'linting'", specifier = "==0.22.3.20260223" }, { name = "types-paramiko", marker = "extra == 'linting'", specifier = "==4.0.0.20250822" }, { name = "types-pillow", marker = "extra == 'linting'", specifier = "==10.2.0.20240822" }, { name = "types-psutil", marker = "extra == 'linting'", specifier = "==7.2.2.20260130" }, @@ -3747,11 +3747,11 @@ wheels = [ [[package]] name = "types-docutils" -version = "0.22.3.20251115" +version = "0.22.3.20260223" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/d7/576ec24bf61a280f571e1f22284793adc321610b9bcfba1bf468cf7b334f/types_docutils-0.22.3.20251115.tar.gz", hash = "sha256:0f79ea6a7bd4d12d56c9f824a0090ffae0ea4204203eb0006392906850913e16", size = 56828, upload-time = "2025-11-15T02:59:57.371Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/33/92c0129283363e3b3ba270bf6a2b7d077d949d2f90afc4abaf6e73578563/types_docutils-0.22.3.20260223.tar.gz", hash = "sha256:e90e868da82df615ea2217cf36dff31f09660daa15fc0f956af53f89c1364501", size = 57230, upload-time = "2026-02-23T04:11:21.806Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/01/61ac9eb38f1f978b47443dc6fd2e0a3b0f647c2da741ddad30771f1b2b6f/types_docutils-0.22.3.20251115-py3-none-any.whl", hash = "sha256:c6e53715b65395d00a75a3a8a74e352c669bc63959e65a207dffaa22f4a2ad6e", size = 91951, upload-time = "2025-11-15T02:59:56.413Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c7/a4ae6a75d5b07d63089d5c04d450a0de4a5d48ffcb84b95659b22d3885fe/types_docutils-0.22.3.20260223-py3-none-any.whl", hash = "sha256:cc2d6b7560a28e351903db0989091474aa619ad287843a018324baee9c4d9a8f", size = 91969, upload-time = "2026-02-23T04:11:20.966Z" }, ] [[package]]