refactor(category): replace cache usage with model property for min/max price

remove redundant cache lookups for `min_price` and `max_price` in the category model by leveraging cached properties. minimizes complexity and improves maintainability while ensuring consistent behavior.
This commit is contained in:
Egor Pavlovich Gorbunov 2026-02-25 12:19:39 +03:00
parent 7efc19e081
commit f664b088a4
9 changed files with 90 additions and 57 deletions

View file

@ -3,8 +3,7 @@ from contextlib import suppress
from typing import Any from typing import Any
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.db.models import QuerySet
from django.db.models import Max, Min, QuerySet
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from graphene import ( from graphene import (
UUID, UUID,
@ -279,21 +278,10 @@ class CategoryType(DjangoObjectType):
return self.filterable_attributes return self.filterable_attributes
def resolve_min_max_prices(self: Category, _info): 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 { return {
"min_price": min_max_prices["min_price"], "min_price": self.min_price,
"max_price": min_max_prices["max_price"], "max_price": self.max_price,
} }
def resolve_brands(self: Category, info) -> QuerySet[Brand]: def resolve_brands(self: Category, info) -> QuerySet[Brand]:

View file

@ -14,7 +14,7 @@ class Command(BaseCommand):
# Group stocks by (product, vendor) # Group stocks by (product, vendor)
stocks_by_group = defaultdict(list) stocks_by_group = defaultdict(list)
for stock in Stock.objects.all().order_by("modified"): 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] = [] stock_deletions: list[str] = []
for group in stocks_by_group.values(): for group in stocks_by_group.values():

View file

@ -386,9 +386,9 @@ class Command(BaseCommand):
if created: if created:
if "name_ru" in prod_data: 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: 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() product.save()
Stock.objects.create( Stock.objects.create(

View file

@ -2,7 +2,7 @@ import datetime
import json import json
import logging import logging
from contextlib import suppress from contextlib import suppress
from typing import Any, Iterable, Self from typing import TYPE_CHECKING, Any, Iterable, Self
from constance import config from constance import config
from django.conf import settings from django.conf import settings
@ -29,6 +29,7 @@ from django.db.models import (
JSONField, JSONField,
ManyToManyField, ManyToManyField,
Max, Max,
Min,
OneToOneField, OneToOneField,
PositiveIntegerField, PositiveIntegerField,
QuerySet, QuerySet,
@ -72,6 +73,9 @@ from engine.core.validators import validate_category_image_dimensions
from engine.payments.models import Transaction from engine.payments.models import Transaction
from schon.utils.misc import create_object from schon.utils.misc import create_object
if TYPE_CHECKING:
from django.db.models import Manager
logger = logging.getLogger(__name__) 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." " This can be useful for categorizing and managing attributes more effectively in acomplex system."
) )
if TYPE_CHECKING:
attributes: Manager["Attribute"]
is_publicly_visible = True is_publicly_visible = True
parent = ForeignKey( parent = ForeignKey(
"self", "self",
@ -266,6 +273,10 @@ class Category(NiceModel, MPTTModel):
" as well as assign attributes like images, tags, or priority." " as well as assign attributes like images, tags, or priority."
) )
if TYPE_CHECKING:
products: Manager["Product"]
children: Manager["Category"]
is_publicly_visible = True is_publicly_visible = True
image = ImageField( image = ImageField(
@ -450,6 +461,20 @@ class Category(NiceModel, MPTTModel):
is_active=True, is_active=True,
).distinct() ).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: class Meta:
verbose_name = _("category") verbose_name = _("category")
verbose_name_plural = _("categories") verbose_name_plural = _("categories")
@ -604,6 +629,12 @@ class Product(NiceModel):
" its associated information within an application." " 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 is_publicly_visible = True
category = ForeignKey( category = ForeignKey(
@ -1274,6 +1305,10 @@ class Order(NiceModel):
" Equally, functionality supports managing the products in the order lifecycle." " 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 is_publicly_visible = False
billing_address = ForeignKey( billing_address = ForeignKey(
@ -1442,7 +1477,8 @@ class Order(NiceModel):
if promotions.exists(): if promotions.exists():
buy_price -= round( 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( order_product, is_created = OrderProduct.objects.get_or_create(
@ -1487,7 +1523,7 @@ class Order(NiceModel):
order_product.delete() order_product.delete()
return self return self
if order_product.quantity == 1: if order_product.quantity == 1:
self.order_products.remove(order_product) self.order_products.remove(order_product) # ty: ignore[unresolved-attribute]
order_product.delete() order_product.delete()
else: else:
order_product.quantity -= 1 order_product.quantity -= 1
@ -1510,7 +1546,7 @@ class Order(NiceModel):
_("you cannot remove products from an order that is not a pending one") _("you cannot remove products from an order that is not a pending one")
) )
for order_product in self.order_products.all(): 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() order_product.delete()
return self return self
@ -1522,7 +1558,7 @@ class Order(NiceModel):
try: try:
product = Product.objects.get(uuid=product_uuid) product = Product.objects.get(uuid=product_uuid)
order_product = self.order_products.get(product=product, order=self) 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() order_product.delete()
except Product.DoesNotExist as dne: except Product.DoesNotExist as dne:
name = "Product" name = "Product"
@ -1788,6 +1824,8 @@ class Order(NiceModel):
crm_links = OrderCrmLink.objects.filter(order=self) crm_links = OrderCrmLink.objects.filter(order=self)
if crm_links.exists(): if crm_links.exists():
crm_link = crm_links.first() crm_link = crm_links.first()
if not crm_link:
return False
crm_integration = create_object( crm_integration = create_object(
crm_link.crm.integration_location, crm_link.crm.name crm_link.crm.integration_location, crm_link.crm.name
) )
@ -1894,6 +1932,9 @@ class OrderProduct(NiceModel):
"and stores a reference to them." "and stores a reference to them."
) )
if TYPE_CHECKING:
download: "DigitalAssetDownload"
is_publicly_visible = False is_publicly_visible = False
buy_price = FloatField( buy_price = FloatField(

View file

@ -174,7 +174,7 @@ def get_top_returned_products(
p = product_by_id[pid] p = product_by_id[pid]
img = "" img = ""
with suppress(Exception): 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( result.append(
{ {
"name": p.name, "name": p.name,

View file

@ -8,7 +8,7 @@ from datetime import datetime
from decimal import Decimal from decimal import Decimal
from io import BytesIO from io import BytesIO
from math import ceil, log10 from math import ceil, log10
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, TypeVar
from constance import config from constance import config
from django.conf import settings from django.conf import settings
@ -36,6 +36,8 @@ from engine.payments.errors import RatesError
from engine.payments.utils import get_rates from engine.payments.utils import get_rates
from schon.utils.misc import LoggingError, LogLevel from schon.utils.misc import LoggingError, LogLevel
_BrandOrCategory = TypeVar("_BrandOrCategory", Brand, Category)
if TYPE_CHECKING: if TYPE_CHECKING:
from engine.core.models import OrderProduct from engine.core.models import OrderProduct
@ -320,8 +322,8 @@ class AbstractVendor(ABC):
@staticmethod @staticmethod
def _auto_resolver_helper( def _auto_resolver_helper(
model: type[Brand] | type[Category], resolving_name: str model: type[_BrandOrCategory], resolving_name: str
) -> Brand | Category | None: ) -> _BrandOrCategory | None:
"""Internal helper for resolving Brand/Category by name with deduplication.""" """Internal helper for resolving Brand/Category by name with deduplication."""
queryset = model.objects.filter(name=resolving_name) queryset = model.objects.filter(name=resolving_name)
if not queryset.exists(): if not queryset.exists():
@ -672,6 +674,8 @@ class AbstractVendor(ABC):
.order_by("uuid") .order_by("uuid")
.first() .first()
) )
if not attribute:
return None
fields_to_update: list[str] = [] fields_to_update: list[str] = []
if not attribute.is_active: if not attribute.is_active:
attribute.is_active = True attribute.is_active = True

View file

@ -607,7 +607,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
product = Product.objects.filter(pk=wished_first["products"]).first() product = Product.objects.filter(pk=wished_first["products"]).first()
if product: if product:
img = ( 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 = { most_wished = {
"name": product.name, "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: if not pid or pid not in product_by_id:
continue continue
p = product_by_id[pid] 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( most_wished_list.append(
{ {
"name": p.name, "name": p.name,
@ -687,10 +687,10 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
.order_by("total_qty")[:5] .order_by("total_qty")[:5]
) )
for p in products: for p in products:
qty = int(p.total_qty or 0) qty = int(p.total_qty or 0) # ty: ignore[possibly-missing-attribute]
img = "" img = ""
with suppress(Exception): 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( low_stock_list.append(
{ {
"name": p.name, "name": p.name,
@ -734,7 +734,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
product = Product.objects.filter(pk=popular_first["product"]).first() product = Product.objects.filter(pk=popular_first["product"]).first()
if product: if product:
img = ( 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 = { most_popular = {
"name": product.name, "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: if not pid or pid not in product_by_id:
continue continue
p = product_by_id[pid] 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( most_popular_list.append(
{ {
"name": p.name, "name": p.name,

View file

@ -22,7 +22,7 @@ dependencies = [
"django-elasticsearch-dsl==8.2", "django-elasticsearch-dsl==8.2",
"django-extensions==4.1", "django-extensions==4.1",
"django-filter==25.2", "django-filter==25.2",
"django-health-check==4.0.4", "django-health-check==4.0.6",
"django-import-export[all]==4.4.0", "django-import-export[all]==4.4.0",
"django-json-widget==2.1.1", "django-json-widget==2.1.1",
"django-model-utils==5.0.0", "django-model-utils==5.0.0",
@ -90,10 +90,10 @@ linting = [
"types-paramiko==4.0.0.20250822", "types-paramiko==4.0.0.20250822",
"types-psutil==7.2.2.20260130", "types-psutil==7.2.2.20260130",
"types-pillow==10.2.0.20240822", "types-pillow==10.2.0.20240822",
"types-docutils==0.22.3.20251115", "types-docutils==0.22.3.20260223",
"types-six==1.17.0.20251009", "types-six==1.17.0.20251009",
] ]
openai = ["openai==2.21.0"] openai = ["openai==2.24.0"]
jupyter = ["jupyter==1.1.1"] jupyter = ["jupyter==1.1.1"]
[tool.uv] [tool.uv]

42
uv.lock
View file

@ -388,11 +388,11 @@ wheels = [
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.1.4" version = "2026.2.25"
source = { registry = "https://pypi.org/simple" } 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 = [ 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]] [[package]]
@ -880,15 +880,15 @@ wheels = [
[[package]] [[package]]
name = "django-health-check" name = "django-health-check"
version = "4.0.4" version = "4.0.6"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "django" }, { name = "django" },
{ name = "dnspython" }, { 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 = [ 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]] [[package]]
@ -1920,7 +1920,7 @@ wheels = [
[[package]] [[package]]
name = "jupyterlab" name = "jupyterlab"
version = "4.5.4" version = "4.5.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "async-lru" }, { name = "async-lru" },
@ -1937,9 +1937,9 @@ dependencies = [
{ name = "tornado" }, { name = "tornado" },
{ name = "traitlets" }, { 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 = [ 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]] [[package]]
@ -2281,7 +2281,7 @@ wheels = [
[[package]] [[package]]
name = "notebook" name = "notebook"
version = "7.5.3" version = "7.5.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "jupyter-server" }, { name = "jupyter-server" },
@ -2290,9 +2290,9 @@ dependencies = [
{ name = "notebook-shim" }, { name = "notebook-shim" },
{ name = "tornado" }, { 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 = [ 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]] [[package]]
@ -2358,7 +2358,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/97/73/8ade73f6749177003
[[package]] [[package]]
name = "openai" name = "openai"
version = "2.21.0" version = "2.24.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "anyio" }, { name = "anyio" },
@ -2370,9 +2370,9 @@ dependencies = [
{ name = "tqdm" }, { name = "tqdm" },
{ name = "typing-extensions" }, { 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 = [ 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]] [[package]]
@ -3440,7 +3440,7 @@ requires-dist = [
{ name = "django-elasticsearch-dsl", specifier = "==8.2" }, { name = "django-elasticsearch-dsl", specifier = "==8.2" },
{ name = "django-extensions", specifier = "==4.1" }, { name = "django-extensions", specifier = "==4.1" },
{ name = "django-filter", specifier = "==25.2" }, { 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-import-export", extras = ["all"], specifier = "==4.4.0" },
{ name = "django-json-widget", specifier = "==2.1.1" }, { name = "django-json-widget", specifier = "==2.1.1" },
{ name = "django-md-field", specifier = "==0.1.0" }, { name = "django-md-field", specifier = "==0.1.0" },
@ -3471,7 +3471,7 @@ requires-dist = [
{ name = "graphene-file-upload", specifier = "==1.3.0" }, { name = "graphene-file-upload", specifier = "==1.3.0" },
{ name = "httpx", specifier = "==0.28.1" }, { name = "httpx", specifier = "==0.28.1" },
{ name = "jupyter", marker = "extra == 'jupyter'", specifier = "==1.1.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 = "opentelemetry-instrumentation-django", specifier = "==0.60b1" },
{ name = "paramiko", specifier = "==4.0.0" }, { name = "paramiko", specifier = "==4.0.0" },
{ name = "pillow", specifier = "==12.1.1" }, { name = "pillow", specifier = "==12.1.1" },
@ -3490,7 +3490,7 @@ requires-dist = [
{ 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.16" }, { 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-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" },
{ name = "types-psutil", marker = "extra == 'linting'", specifier = "==7.2.2.20260130" }, { name = "types-psutil", marker = "extra == 'linting'", specifier = "==7.2.2.20260130" },
@ -3747,11 +3747,11 @@ wheels = [
[[package]] [[package]]
name = "types-docutils" name = "types-docutils"
version = "0.22.3.20251115" version = "0.22.3.20260223"
source = { registry = "https://pypi.org/simple" } 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 = [ 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]] [[package]]