Features:
1) Add new `test_graphene` test module for expanded testing coverage; 2) Introduce `test_drf` module in `engine/blog` for improved API testing; Fixes: 1) Remove unnecessary `--extra testing` flag from Dockerfile to streamline dependencies; 2) Update `uv.lock` with newer versions of dependencies (`certifi`, `coverage`, `django-constance`) for enhanced security and functionality; Extra: 1) Remove deprecated packages (`bandit`, `cfgv`, `distlib`) from `uv.lock` for cleanup; 2) Adjust `uv.lock` content and formatting to be consistent with updated dependencies.
This commit is contained in:
parent
425464114c
commit
3228a89d4b
26 changed files with 530 additions and 291 deletions
|
|
@ -47,7 +47,7 @@ COPY pyproject.toml pyproject.toml
|
|||
COPY uv.lock uv.lock
|
||||
|
||||
RUN set -eux; \
|
||||
uv sync --extra graph --extra worker --extra openai --extra testing --locked
|
||||
uv sync --extra graph --extra worker --extra openai --locked
|
||||
|
||||
COPY ./scripts/Docker/app-entrypoint.sh /usr/local/bin/app-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/app-entrypoint.sh
|
||||
|
|
|
|||
0
engine/core/tests/__init__.py
Normal file
0
engine/core/tests/__init__.py
Normal file
0
engine/core/tests/test_drf.py
Normal file
0
engine/core/tests/test_drf.py
Normal file
0
engine/core/tests/test_graphene.py
Normal file
0
engine/core/tests/test_graphene.py
Normal file
|
|
@ -5,6 +5,10 @@ class UnknownGatewayError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class BadLimitsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AbstractGateway:
|
||||
@staticmethod
|
||||
def process_transaction(transaction: Any) -> None:
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ from typing import Any
|
|||
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from engine.payments.models import Balance, Transaction, Gateway
|
||||
from engine.payments.gateways import BadLimitsError
|
||||
from engine.payments.models import Balance, Gateway, Transaction
|
||||
from engine.payments.utils.emailing import balance_deposit_email
|
||||
from engine.vibes_auth.models import User
|
||||
|
||||
|
|
@ -27,7 +29,21 @@ def process_transaction_changes(instance: Transaction, created: bool, **kwargs:
|
|||
instance.gateway = Gateway.objects.can_be_used().first()
|
||||
try:
|
||||
gateway = instance.gateway.get_integration_class_object()
|
||||
gateway.process_transaction(instance) # type: ignore [union-attr]
|
||||
if (
|
||||
instance.gateway.minimum_transaction_amount
|
||||
<= instance.amount
|
||||
<= instance.gateway.maximum_transaction_amount
|
||||
):
|
||||
gateway.process_transaction(instance) # type: ignore [union-attr]
|
||||
else:
|
||||
raise BadLimitsError(
|
||||
_(
|
||||
f"the transaction amount didn't fit into allowed limits: "
|
||||
f"{instance.gateway.minimum_transaction_amount} "
|
||||
f"!<= {instance.amount} "
|
||||
f"!<= {instance.gateway.maximum_transaction_amount}"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
instance.process = {"status": "ERRORED", "error": str(e)}
|
||||
logger.error(f"Error processing transaction {instance.uuid}: {e}\n{traceback.format_exc()}")
|
||||
|
|
|
|||
0
engine/payments/tests/__init__.py
Normal file
0
engine/payments/tests/__init__.py
Normal file
0
engine/payments/tests/test_drf.py
Normal file
0
engine/payments/tests/test_drf.py
Normal file
0
engine/payments/tests/test_graphene.py
Normal file
0
engine/payments/tests/test_graphene.py
Normal file
|
|
@ -1,4 +1,6 @@
|
|||
import logging
|
||||
import traceback
|
||||
from contextlib import suppress
|
||||
from hmac import compare_digest
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -239,21 +241,19 @@ class VerifyJSONWebToken(BaseMutation):
|
|||
|
||||
token_is_valid = Boolean()
|
||||
user = Field(UserType)
|
||||
detail = String()
|
||||
|
||||
def mutate(self, info, token):
|
||||
serializer = TokenVerifySerializer(data={"token": token})
|
||||
try:
|
||||
with suppress(Exception):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user_uuid = serializer.validated_data["user"]["uuid"]
|
||||
user = User.objects.get(pk=user_uuid)
|
||||
# noinspection PyTypeChecker
|
||||
return VerifyJSONWebToken(
|
||||
token_is_valid=True,
|
||||
user=user,
|
||||
)
|
||||
except ValidationError:
|
||||
# noinspection PyTypeChecker
|
||||
return VerifyJSONWebToken(token_is_valid=False, user=None)
|
||||
return VerifyJSONWebToken(token_is_valid=True, user=user)
|
||||
detail = traceback.format_exc() if settings.DEBUG else ""
|
||||
# noinspection PyTypeChecker
|
||||
return VerifyJSONWebToken(token_is_valid=False, user=None, detail=detail)
|
||||
|
||||
|
||||
class ActivateUser(BaseMutation):
|
||||
|
|
|
|||
0
engine/vibes_auth/tests/__init__.py
Normal file
0
engine/vibes_auth/tests/__init__.py
Normal file
144
engine/vibes_auth/tests/test_drf.py
Normal file
144
engine/vibes_auth/tests/test_drf.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
from base64 import urlsafe_b64encode
|
||||
from io import BytesIO
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from engine.vibes_auth.models import User
|
||||
|
||||
|
||||
class DRFViewsTests(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client = APIClient()
|
||||
|
||||
def test_token_obtain_pair_success(self):
|
||||
user = User.objects.create_user(email="user@example.com", password="Str0ngPass!word", is_active=True)
|
||||
url = reverse("vibes_auth:token_create")
|
||||
resp = self.client.post(url, {"email": user.email, "password": "Str0ngPass!word"}, format="json")
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
data = resp.json()
|
||||
self.assertIn("access", data, data)
|
||||
self.assertTrue(data["access"], data)
|
||||
self.assertIn("refresh", data, data)
|
||||
self.assertTrue(data["refresh"], data)
|
||||
self.assertEqual(data["user"]["email"], user.email, data)
|
||||
|
||||
def test_token_obtain_pair_invalid_credentials(self):
|
||||
User.objects.create_user(email="user@example.com", password="Str0ngPass!word", is_active=True)
|
||||
url = reverse("vibes_auth:token_create")
|
||||
resp = self.client.post(url, {"email": "user@example.com", "password": "wrong"}, format="json")
|
||||
self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_token_obtain_ratelimited(self):
|
||||
url = reverse("vibes_auth:token_create")
|
||||
for _ in range(0, 10):
|
||||
self.client.post(url, {"email": "user@example.com", "password": "wrong"}, format="json")
|
||||
resp = self.client.post(url, {"email": "user@example.com", "password": "wrong"}, format="json")
|
||||
self.assertEqual(resp.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
|
||||
|
||||
def test_token_refresh_and_verify_flow(self):
|
||||
user = User.objects.create_user(email="user@example.com", password="Str0ngPass!word", is_active=True)
|
||||
tokens = RefreshToken.for_user(user)
|
||||
|
||||
refresh_url = reverse("vibes_auth:token_refresh")
|
||||
resp_refresh = self.client.post(refresh_url, {"refresh": str(tokens)}, format="json")
|
||||
self.assertEqual(resp_refresh.status_code, status.HTTP_200_OK)
|
||||
access = resp_refresh.json()["access"]
|
||||
|
||||
verify_url = reverse("vibes_auth:token_verify")
|
||||
resp_verify = self.client.post(verify_url, {"token": access}, format="json")
|
||||
self.assertEqual(resp_verify.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(resp_verify.json()["token"])
|
||||
self.assertEqual(resp_verify.json()["user"]["email"], user.email)
|
||||
|
||||
def test_token_verify_invalid_token(self):
|
||||
verify_url = reverse("vibes_auth:token_verify")
|
||||
resp = self.client.post(verify_url, {"token": "malformed"}, format="json")
|
||||
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("detail", resp.json(), resp.json())
|
||||
|
||||
def test_user_create_and_activate_flow(self):
|
||||
url_create = reverse("vibes_auth:users-list")
|
||||
payload = {
|
||||
"email": "new@example.com",
|
||||
"password": "Str0ngPass!word",
|
||||
"confirm_password": "Str0ngPass!word",
|
||||
"first_name": "New",
|
||||
"last_name": "User",
|
||||
}
|
||||
resp = self.client.post(url_create, payload, format="json")
|
||||
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
|
||||
user_uuid = resp.json()["uuid"]
|
||||
user = User.objects.get(uuid=user_uuid)
|
||||
self.assertFalse(user.is_active)
|
||||
|
||||
activate_url = reverse("vibes_auth:users-activate")
|
||||
uidb64 = urlsafe_b64encode(str(user.uuid).encode()).decode()
|
||||
token_b64 = urlsafe_b64encode(str(user.activation_token).encode()).decode()
|
||||
resp_act = self.client.post(activate_url, {"uidb_64": uidb64, "token": token_b64}, format="json")
|
||||
self.assertEqual(resp_act.status_code, status.HTTP_200_OK)
|
||||
user.refresh_from_db()
|
||||
self.assertTrue(user.is_active and user.is_verified)
|
||||
|
||||
def test_reset_password_triggers_task(self):
|
||||
user = User.objects.create_user(email="user@example.com", password="Str0ngPass!word", is_active=True)
|
||||
|
||||
with patch("engine.vibes_auth.viewsets.send_reset_password_email_task.delay") as mocked_delay:
|
||||
url = reverse("vibes_auth:users-reset-password")
|
||||
resp = self.client.post(url, {"email": user.email}, format="json")
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
mocked_delay.assert_called_once()
|
||||
|
||||
def test_confirm_password_reset_success(self):
|
||||
user = User.objects.create_user(email="user@example.com", password="OldPass!123", is_active=True)
|
||||
gen = PasswordResetTokenGenerator()
|
||||
token = gen.make_token(user)
|
||||
uidb64 = urlsafe_b64encode(str(user.uuid).encode()).decode()
|
||||
|
||||
url = reverse("vibes_auth:users-confirm-password-reset")
|
||||
new_pass = "NewPass!12345"
|
||||
resp = self.client.post(
|
||||
url,
|
||||
{
|
||||
"uidb_64": uidb64,
|
||||
"token": token,
|
||||
"password": new_pass,
|
||||
"confirm_password": new_pass,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK, resp.json())
|
||||
obtain_url = reverse("vibes_auth:token_create")
|
||||
r2 = self.client.post(obtain_url, {"email": user.email, "password": new_pass}, format="json")
|
||||
self.assertEqual(r2.status_code, status.HTTP_200_OK, resp.json())
|
||||
|
||||
def test_upload_avatar_permission_enforced(self):
|
||||
owner = User.objects.create_user(email="owner@example.com", password="Str0ngPass!word", is_active=True)
|
||||
stranger = User.objects.create_user(email="stranger@example.com", password="Str0ngPass!word", is_active=True)
|
||||
|
||||
access = str(RefreshToken.for_user(stranger).access_token)
|
||||
self.client.credentials(HTTP_X_EVIBES_AUTH=f"Bearer {access}")
|
||||
|
||||
url = reverse("vibes_auth:users-upload-avatar", kwargs={"pk": owner.pk})
|
||||
file_content = BytesIO(b"fake image content")
|
||||
file = SimpleUploadedFile("avatar.png", file_content.getvalue(), content_type="image/png")
|
||||
resp = self.client.put(url, {"avatar": file})
|
||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_merge_recently_viewed_permission_enforced(self):
|
||||
owner = User.objects.create_user(email="owner@example.com", password="Str0ngPass!word", is_active=True)
|
||||
stranger = User.objects.create_user(email="stranger@example.com", password="Str0ngPass!word", is_active=True)
|
||||
|
||||
access = str(RefreshToken.for_user(stranger).access_token)
|
||||
self.client.credentials(HTTP_X_EVIBES_AUTH=f"Bearer {access}")
|
||||
|
||||
url = reverse("vibes_auth:users-merge-recently-viewed", kwargs={"pk": owner.pk})
|
||||
resp = self.client.put(url, {"product_uuids": []}, format="json")
|
||||
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
|
||||
106
engine/vibes_auth/tests/test_graphene.py
Normal file
106
engine/vibes_auth/tests/test_graphene.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import base64
|
||||
from typing import Any
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from engine.vibes_auth.models import User
|
||||
|
||||
|
||||
class GraphQLAuthTests(TestCase):
|
||||
def graphql(self, query: str, variables: dict | None = None):
|
||||
url = reverse("graphql-platform")
|
||||
payload: dict[str, Any] = {"query": query}
|
||||
if variables:
|
||||
payload["variables"] = variables
|
||||
response = self.client.post(url, data=payload, content_type="application/json")
|
||||
self.assertEqual(response.status_code, 200, response.json())
|
||||
return response.json()
|
||||
|
||||
def test_obtain_refresh_verify_jwt_via_graphql(self):
|
||||
user = User.objects.create_user(email="user@example.com", password="Str0ngPass!word", is_active=True)
|
||||
|
||||
data = self.graphql(
|
||||
"""
|
||||
mutation {
|
||||
obtainJwtToken(email: "user@example.com", password: "Str0ngPass!word") {
|
||||
accessToken
|
||||
refreshToken
|
||||
user { email uuid }
|
||||
}
|
||||
}
|
||||
""",
|
||||
)
|
||||
self.assertNotIn("errors", data)
|
||||
payload = data["data"]["obtainJwtToken"]
|
||||
self.assertEqual(payload["user"]["email"], user.email)
|
||||
refresh = payload["refreshToken"]
|
||||
|
||||
data2 = self.graphql(
|
||||
f"""
|
||||
mutation {{
|
||||
refreshJwtToken(refreshToken: "{refresh}") {{
|
||||
accessToken
|
||||
refreshToken
|
||||
user {{ email }}
|
||||
}}
|
||||
}}
|
||||
""",
|
||||
)
|
||||
self.assertNotIn("errors", data2)
|
||||
access2 = data2["data"]["refreshJwtToken"]["accessToken"]
|
||||
|
||||
data3 = self.graphql(
|
||||
f"""
|
||||
mutation {{
|
||||
verifyJwtToken(token: "{access2}") {{
|
||||
tokenIsValid
|
||||
user {{ email }}
|
||||
}}
|
||||
}}
|
||||
""",
|
||||
)
|
||||
self.assertTrue(data3["data"]["verifyJwtToken"]["tokenIsValid"])
|
||||
self.assertEqual(data3["data"]["verifyJwtToken"]["user"]["email"], user.email)
|
||||
|
||||
def test_create_user_and_activate_graphql(self):
|
||||
data = self.graphql(
|
||||
"""
|
||||
mutation {
|
||||
createUser(email:"new@example.com", password:"Str0ngPass!word", confirmPassword:"Str0ngPass!word") {
|
||||
success
|
||||
}
|
||||
}
|
||||
""",
|
||||
)
|
||||
self.assertTrue(data["data"]["createUser"]["success"])
|
||||
user = User.objects.get(email="new@example.com")
|
||||
self.assertFalse(user.is_active)
|
||||
|
||||
uid = base64.b64encode(str(user.uuid).encode()).decode()
|
||||
token = base64.b64encode(str(user.activation_token).encode()).decode()
|
||||
data2 = self.graphql(
|
||||
f"""
|
||||
mutation {{
|
||||
activateUser(uid:"{uid}", token:"{token}") {{
|
||||
success
|
||||
}}
|
||||
}}
|
||||
""",
|
||||
)
|
||||
self.assertTrue(data2["data"]["activateUser"]["success"], data2)
|
||||
user.refresh_from_db()
|
||||
self.assertTrue(user.is_active and user.is_verified, user)
|
||||
|
||||
def test_verify_json_web_token_invalid_graphql(self):
|
||||
data = self.graphql(
|
||||
"""
|
||||
mutation {
|
||||
verifyJwtToken(token: "invalid") {
|
||||
tokenIsValid
|
||||
user { email }
|
||||
}
|
||||
}
|
||||
""",
|
||||
)
|
||||
self.assertFalse(data["data"]["verifyJwtToken"]["tokenIsValid"], data)
|
||||
87
engine/vibes_auth/tests/test_messaging.py
Normal file
87
engine/vibes_auth/tests/test_messaging.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.test import TestCase
|
||||
|
||||
from engine.vibes_auth.models import User
|
||||
from engine.vibes_auth.messaging import auth as auth_module
|
||||
|
||||
|
||||
class MessagingAuthTests(TestCase):
|
||||
def test_extract_jwt_from_subprotocols_cases(self):
|
||||
fn = auth_module._extract_jwt_from_subprotocols
|
||||
self.assertIsNone(fn(None))
|
||||
self.assertIsNone(fn([]))
|
||||
self.assertEqual(fn(["bearer", "abc.token"]), "abc.token")
|
||||
self.assertEqual(fn(["Bearer", "abc"]), "abc")
|
||||
self.assertEqual(fn(["single-token"]), "single-token")
|
||||
self.assertIsNone(fn([""]))
|
||||
self.assertIsNone(fn(["Bearer", ""]))
|
||||
|
||||
def test_jwt_middleware_sets_anonymous_without_token(self):
|
||||
captured = {}
|
||||
|
||||
async def inner_app(scope_dict, _receive, _send):
|
||||
captured["is_anon"] = isinstance(scope_dict["user"], AnonymousUser) or scope_dict["user"].is_anonymous
|
||||
|
||||
middleware = auth_module.JWTAuthMiddleware(inner_app)
|
||||
|
||||
scope = {"type": "websocket", "subprotocols": []}
|
||||
|
||||
async def dummy_receive():
|
||||
return {"type": "websocket.disconnect"}
|
||||
|
||||
async def dummy_send(_message):
|
||||
return None
|
||||
|
||||
asyncio.run(middleware(scope, dummy_receive, dummy_send))
|
||||
self.assertTrue(captured.get("is_anon"))
|
||||
|
||||
def test_jwt_middleware_sets_user_with_valid_token(self):
|
||||
user = User.objects.create_user(email="user@example.com", password="Str0ngPass!word")
|
||||
|
||||
class FakeAuth:
|
||||
def authenticate(self, _request):
|
||||
return user, "token"
|
||||
|
||||
captured = {}
|
||||
|
||||
async def inner_app(scope_dict, _receive, _send):
|
||||
captured["user_id"] = getattr(scope_dict["user"], "pk", None)
|
||||
|
||||
middleware = auth_module.JWTAuthMiddleware(inner_app)
|
||||
scope = {"type": "websocket", "subprotocols": ["bearer", "abc.def"]}
|
||||
|
||||
async def dummy_receive():
|
||||
return {"type": "websocket.disconnect"}
|
||||
|
||||
async def dummy_send(_message):
|
||||
return None
|
||||
|
||||
with patch.object(auth_module, "JWTAuthentication", FakeAuth):
|
||||
asyncio.run(middleware(scope, dummy_receive, dummy_send))
|
||||
self.assertEqual(captured.get("user_id"), user.pk)
|
||||
|
||||
def test_jwt_middleware_handles_bad_token_gracefully(self):
|
||||
class FakeAuth:
|
||||
def authenticate(self, _request):
|
||||
raise Exception("bad token")
|
||||
|
||||
captured = {}
|
||||
|
||||
async def inner_app(scope_dict, _receive, _send):
|
||||
captured["is_anon"] = isinstance(scope_dict["user"], AnonymousUser) or scope_dict["user"].is_anonymous
|
||||
|
||||
middleware = auth_module.JWTAuthMiddleware(inner_app)
|
||||
scope = {"type": "websocket", "subprotocols": ["bearer", "bad.token"]}
|
||||
|
||||
async def dummy_receive():
|
||||
return {"type": "websocket.disconnect"}
|
||||
|
||||
async def dummy_send(_message):
|
||||
return None
|
||||
|
||||
with patch.object(auth_module, "JWTAuthentication", FakeAuth):
|
||||
asyncio.run(middleware(scope, dummy_receive, dummy_send))
|
||||
self.assertTrue(captured.get("is_anon"))
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Type
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.decorators import method_decorator
|
||||
|
|
@ -9,6 +9,7 @@ from drf_spectacular.utils import (
|
|||
extend_schema_view,
|
||||
)
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny, BasePermission
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_simplejwt.exceptions import TokenError
|
||||
|
|
@ -36,8 +37,12 @@ class TokenObtainPairView(TokenViewBase):
|
|||
|
||||
serializer_class = TokenObtainPairSerializer # type: ignore [assignment]
|
||||
_serializer_class = TokenObtainPairSerializer # type: ignore [assignment]
|
||||
permission_classes: list[Type[BasePermission]] = [
|
||||
AllowAny,
|
||||
]
|
||||
authentication_classes: list[str] = []
|
||||
|
||||
@method_decorator(ratelimit(key="ip", rate="10/h" if not settings.DEBUG else "888/h"))
|
||||
@method_decorator(ratelimit(key="ip", rate="10/h"))
|
||||
def post(self, request: Request, *args: list[Any], **kwargs: dict[Any, Any]) -> Response:
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
|
|
@ -55,8 +60,12 @@ class TokenRefreshView(TokenViewBase):
|
|||
|
||||
serializer_class = TokenRefreshSerializer # type: ignore [assignment]
|
||||
_serializer_class = TokenRefreshSerializer # type: ignore [assignment]
|
||||
permission_classes: list[Type[BasePermission]] = [
|
||||
AllowAny,
|
||||
]
|
||||
authentication_classes: list[str] = []
|
||||
|
||||
@method_decorator(ratelimit(key="ip", rate="10/h" if not settings.DEBUG else "888/h"))
|
||||
@method_decorator(ratelimit(key="ip", rate="10/h"))
|
||||
def post(self, request: Request, *args: list[Any], **kwargs: dict[Any, Any]) -> Response:
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
|
|
@ -69,6 +78,10 @@ class TokenVerifyView(TokenViewBase):
|
|||
|
||||
serializer_class = TokenVerifySerializer # type: ignore [assignment]
|
||||
_serializer_class = TokenVerifySerializer # type: ignore [assignment]
|
||||
permission_classes: list[Type[BasePermission]] = [
|
||||
AllowAny,
|
||||
]
|
||||
authentication_classes: list[str] = []
|
||||
|
||||
def post(self, request: Request, *args: list[Any], **kwargs: dict[Any, Any]) -> Response:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class UserViewSet(
|
|||
permission_classes = [AllowAny]
|
||||
|
||||
@action(detail=False, methods=["post"])
|
||||
@method_decorator(ratelimit(key="ip", rate="2/h" if not settings.DEBUG else "888/h"))
|
||||
@method_decorator(ratelimit(key="ip", rate="4/h" if not settings.DEBUG else "888/h"))
|
||||
def reset_password(self, request: Request) -> Response:
|
||||
user = None
|
||||
with suppress(User.DoesNotExist):
|
||||
|
|
@ -64,7 +64,7 @@ class UserViewSet(
|
|||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["put"], permission_classes=[IsAuthenticated])
|
||||
@method_decorator(ratelimit(key="ip", rate="2/h" if not settings.DEBUG else "888/h"))
|
||||
@method_decorator(ratelimit(key="ip", rate="3/h" if not settings.DEBUG else "888/h"))
|
||||
def upload_avatar(self, request: Request, *args, **kwargs) -> Response:
|
||||
user = self.get_object()
|
||||
if request.user != user:
|
||||
|
|
@ -76,7 +76,7 @@ class UserViewSet(
|
|||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@action(detail=False, methods=["post"])
|
||||
@method_decorator(ratelimit(key="ip", rate="2/h" if not settings.DEBUG else "888/h"))
|
||||
@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")): # type: ignore [arg-type]
|
||||
|
|
@ -105,7 +105,7 @@ class UserViewSet(
|
|||
data["received"] = str(request.data)
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@method_decorator(ratelimit(key="ip", rate="3/h" if not settings.DEBUG else "888/h"))
|
||||
@method_decorator(ratelimit(key="ip", rate="5/h" if not settings.DEBUG else "888/h"))
|
||||
def create(self, request: Request, *args, **kwargs) -> Response:
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
|
@ -115,7 +115,7 @@ class UserViewSet(
|
|||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
@action(detail=False, methods=["post"])
|
||||
@method_decorator(ratelimit(key="ip", rate="2/h" if not settings.DEBUG else "888/h"))
|
||||
@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
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from os import getenv
|
|||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import BadRequest, DisallowedHost, PermissionDenied, ValidationError
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.http import HttpResponseForbidden, JsonResponse
|
||||
from django.middleware.common import CommonMiddleware
|
||||
from django.middleware.locale import LocaleMiddleware
|
||||
from django.shortcuts import redirect
|
||||
|
|
@ -12,6 +12,8 @@ from rest_framework_simplejwt.authentication import JWTAuthentication
|
|||
from rest_framework_simplejwt.exceptions import InvalidToken
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
from evibes.utils.misc import RatelimitedError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -104,3 +106,19 @@ class GrapheneLoggingErrorsDebugMiddleware:
|
|||
logger.error(str(e), exc_info=True)
|
||||
capture_exception(e)
|
||||
raise e
|
||||
|
||||
|
||||
class RateLimitMiddleware:
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
return self.get_response(request)
|
||||
|
||||
def process_exception(self, request, exception):
|
||||
if isinstance(exception, RatelimitedError):
|
||||
return JsonResponse(
|
||||
{"error": str(exception), "code": getattr(exception, "code", "rate_limited")}, status=429
|
||||
)
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -9,12 +9,13 @@ from django.core.exceptions import ImproperlyConfigured
|
|||
EVIBES_VERSION = "2025.4"
|
||||
RELEASE_DATE = datetime(2025, 11, 9)
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
INITIALIZED = (BASE_DIR / ".initialized").exists()
|
||||
BASE_DIR: Path = Path(__file__).resolve().parent.parent.parent
|
||||
INITIALIZED: bool = (BASE_DIR / ".initialized").exists()
|
||||
|
||||
SECRET_KEY = getenv("SECRET_KEY", "SUPER_SECRET_KEY")
|
||||
DEBUG = bool(int(getenv("DEBUG", "1")))
|
||||
DEBUG_DATABASE = bool(int(getenv("DEBUG_DATABASE", "1")))
|
||||
SECRET_KEY: str = getenv("SECRET_KEY", "SUPER_SECRET_KEY")
|
||||
DEBUG: bool = bool(int(getenv("DEBUG", "1")))
|
||||
DEBUG_DATABASE: bool = bool(int(getenv("DEBUG_DATABASE", "0")))
|
||||
DEBUG_CELERY: bool = bool(int(getenv("DEBUG_DATABASE", "0")))
|
||||
|
||||
BASE_DOMAIN: str = getenv("EVIBES_BASE_DOMAIN", "localhost")
|
||||
STOREFRONT_DOMAIN: str = getenv("EVIBES_STOREFRONT_DOMAIN", "localhost")
|
||||
|
|
@ -158,6 +159,7 @@ if DEBUG:
|
|||
|
||||
MIDDLEWARE: list[str] = [
|
||||
"evibes.middleware.BlockInvalidHostMiddleware",
|
||||
"evibes.middleware.RateLimitMiddleware",
|
||||
"django_prometheus.middleware.PrometheusBeforeMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
|
|
@ -401,6 +403,8 @@ LANGUAGE_COOKIE_HTTPONLY: bool = True
|
|||
|
||||
DATA_UPLOAD_MAX_NUMBER_FIELDS: int = 8888
|
||||
|
||||
RATELIMIT_EXCEPTION_CLASS: str = "evibes.utils.misc.RatelimitedError"
|
||||
|
||||
ADMINS: list[tuple[str, ...]] = [("Egor Gorbunov", "contact@fureunoir.com")]
|
||||
|
||||
STORAGES: dict[str, Any] = {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ JAZZMIN_SETTINGS = {
|
|||
"url": f"https://api.{BASE_DOMAIN}/docs/swagger", # type: ignore [index]
|
||||
"new_window": True,
|
||||
},
|
||||
{"name": _("Taskboard"), "url": "https://gitlab.com/wiseless/evibes", "new_window": True},
|
||||
{"name": _("Taskboard"), "url": "https://plane.wiseless.xyz/spaces/issues/dd33cb0ab9b04ef08a10f7eefae6d90c/?board=kanban", "new_window": True},
|
||||
{"name": "GitLab", "url": "https://gitlab.com/wiseless/evibes", "new_window": True},
|
||||
{"name": _("Support"), "url": "https://t.me/fureunoir", "new_window": True},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import logging
|
||||
|
||||
from evibes.settings.base import DEBUG, DEBUG_DATABASE
|
||||
from evibes.settings.base import DEBUG, DEBUG_DATABASE, DEBUG_CELERY
|
||||
|
||||
|
||||
class SkipVariableDoesNotExistFilter(logging.Filter):
|
||||
|
|
@ -89,6 +89,11 @@ LOGGING = {
|
|||
"level": "DEBUG" if DEBUG else "INFO",
|
||||
"propagate": True,
|
||||
},
|
||||
"celery.utils": {
|
||||
"handlers": ["console"],
|
||||
"level": "DEBUG" if DEBUG_CELERY else "INFO",
|
||||
"propagate": True,
|
||||
},
|
||||
"django_elasticsearch_dsl": {
|
||||
"handlers": ["console"],
|
||||
"level": "WARNING",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ from enum import Enum
|
|||
from importlib import import_module
|
||||
from typing import Any
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
|
||||
def create_object(module_name: str, class_name: str, *args: list[Any], **kwargs: dict[Any, Any]) -> Any:
|
||||
module = import_module(module_name)
|
||||
|
|
@ -22,3 +24,20 @@ class LogLevel(Enum):
|
|||
ERROR = "error"
|
||||
CRITICAL = "critical"
|
||||
TRACE = "trace"
|
||||
|
||||
|
||||
class RatelimitedError(Exception):
|
||||
|
||||
default_detail = "Rate limit exceeded. Please try again later."
|
||||
default_code = "rate_limited"
|
||||
status_code = 429
|
||||
|
||||
def __init__(self, detail=None, code=None):
|
||||
if detail is None:
|
||||
detail = self.default_detail
|
||||
if code is None:
|
||||
code = self.default_code
|
||||
|
||||
self.detail = detail
|
||||
self.code = code
|
||||
super().__init__(detail)
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ dependencies = [
|
|||
"channels==4.3.1",
|
||||
"channels-redis>=4.3.0",
|
||||
"colorlog==6.10.1",
|
||||
"coverage==7.11.1",
|
||||
"coverage==7.11.3",
|
||||
"click==8.3.0",
|
||||
"cryptography==46.0.3",
|
||||
"django==5.2.8",
|
||||
"django-cacheops==7.2",
|
||||
"django-constance==4.3.3",
|
||||
"django-constance==4.3.4",
|
||||
"django-cors-headers==4.9.0",
|
||||
"django-dbbackup==5.0.1",
|
||||
"django-elasticsearch-dsl==8.2",
|
||||
|
|
@ -58,13 +58,15 @@ dependencies = [
|
|||
"pip>=25.3",
|
||||
"polib==1.2.0",
|
||||
"PyJWT==2.10.1",
|
||||
"pytest==9.0.1",
|
||||
"pytest-django==4.11.1",
|
||||
"python-slugify==8.0.4",
|
||||
"psutil==7.1.3",
|
||||
"psycopg2==2.9.11",
|
||||
"pymdown-extensions==10.16.1",
|
||||
"pymdown-extensions==10.17.1",
|
||||
"redis==7.0.1",
|
||||
"requests==2.32.5",
|
||||
"sentry-sdk[django,celery,opentelemetry]==2.43.0",
|
||||
"sentry-sdk[django,celery,opentelemetry]==2.44.0",
|
||||
"six==1.17.0",
|
||||
"swapper==1.4.0",
|
||||
"uvicorn==0.38.0",
|
||||
|
|
@ -74,7 +76,6 @@ dependencies = [
|
|||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
testing = ["pytest==8.4.2", "pytest-django==4.11.1", "coverage==7.11.1"]
|
||||
graph = ["pygraphviz==1.14; sys_platform != 'win32'"]
|
||||
worker = [
|
||||
"celery==5.5.3",
|
||||
|
|
@ -82,22 +83,16 @@ worker = [
|
|||
"django-celery-results==2.6.0",
|
||||
"celery-prometheus-exporter==1.7.0",
|
||||
]
|
||||
openai = ["openai==2.6.1"]
|
||||
jupyter = ["jupyter==1.1.1"]
|
||||
docs = ["sphinx==8.2.3", "sphinx-rtd-theme==3.0.2", "m2r2==0.3.4"]
|
||||
|
||||
[dependency-groups]
|
||||
testing = ["pytest==8.4.2", "pytest-django==4.11.1", "coverage==7.11.1"]
|
||||
linting = [
|
||||
"isort==7.0.0",
|
||||
"bandit==1.8.6",
|
||||
"pre-commit==4.3.0",
|
||||
"mypy==1.18.2",
|
||||
"mypy-extensions==1.1.0",
|
||||
"ruff==0.14.4",
|
||||
"celery-stubs==0.1.3",
|
||||
]
|
||||
dev = [{ include-group = "testing" }, { include-group = "linting" }]
|
||||
openai = ["openai==2.6.1"]
|
||||
jupyter = ["jupyter==1.1.1"]
|
||||
docs = ["sphinx==8.2.3", "sphinx-rtd-theme==3.0.2", "m2r2==0.3.4"]
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
|
|
|
|||
328
uv.lock
328
uv.lock
|
|
@ -267,21 +267,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 = "bandit"
|
||||
version = "1.8.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "rich" },
|
||||
{ name = "stevedore" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/b5/7eb834e213d6f73aace21938e5e90425c92e5f42abafaf8a6d5d21beed51/bandit-1.8.6.tar.gz", hash = "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b", size = 4240271, upload-time = "2025-07-06T03:10:50.9Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/ca/ba5f909b40ea12ec542d5d7bdd13ee31c4d65f3beed20211ef81c18fa1f3/bandit-1.8.6-py3-none-any.whl", hash = "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0", size = 133808, upload-time = "2025-07-06T03:10:49.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "5.0.0"
|
||||
|
|
@ -413,11 +398,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.10.5"
|
||||
version = "2025.11.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -455,15 +440,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "channels"
|
||||
version = "4.3.1"
|
||||
|
|
@ -614,50 +590,50 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.11.1"
|
||||
version = "7.11.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/12/3e2d2ec71796e0913178478e693a06af6a3bc9f7f9cb899bf85a426d8370/coverage-7.11.1.tar.gz", hash = "sha256:b4b3a072559578129a9e863082a2972a2abd8975bc0e2ec57da96afcd6580a8a", size = 814037, upload-time = "2025-11-07T10:52:41.067Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d2/59/9698d57a3b11704c7b89b21d69e9d23ecf80d538cabb536c8b63f4a12322/coverage-7.11.3.tar.gz", hash = "sha256:0f59387f5e6edbbffec2281affb71cdc85e0776c1745150a3ab9b6c1d016106b", size = 815210, upload-time = "2025-11-10T00:13:17.18Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/31/04af7e42fdb3681e4d73d37bf3f375f0488aa38d1001ee746c7dbfe09643/coverage-7.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:421e2d237dcecdefa9b77cae1aa0dfff5c495f29e053e776172457e289976311", size = 216896, upload-time = "2025-11-07T10:50:31.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/e9/1c3628a1225bdea66295a117cd2bb1d324d9c433c40078b24d50f55448a7/coverage-7.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:08ef89c812072ecd52a862b46e131f75596475d23cc7f5a75410394341d4332f", size = 217261, upload-time = "2025-11-07T10:50:33.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/80/4d4f943da23c432b2bba8664f4eada9b19911081852e8cc89776c61d0b94/coverage-7.11.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bc6e0b2d6ed317810b4e435ffabc31b2d517d6ceb4183dfd6af4748c52d170eb", size = 248742, upload-time = "2025-11-07T10:50:34.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/e1/c4b42f02fbb6ce08e05d7a2b26bcf5df11d3e67a3806e40415f7ab9511e7/coverage-7.11.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b59736704df8b1f8b1dafb36b16f2ef8a952e4410465634442459426bd2319ae", size = 251503, upload-time = "2025-11-07T10:50:36.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/a8/3df60e88f1dabccae4994c6df4a2f23d4cd0eee27fc3ae8f0bb2e78cb538/coverage-7.11.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:843816452d8bfc4c2be72546b3b382850cb91150feaa963ec7d2b665ec9d4768", size = 252590, upload-time = "2025-11-07T10:50:38.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/1c/2b9fae11361b0348c2d3612a8179d2cc8b6b245e8b14d5479c75b9f18613/coverage-7.11.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:19363046125d4a423c25d3d7c90bab3a0230932c16014198f87a6b3960c1b187", size = 249133, upload-time = "2025-11-07T10:50:39.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/2b/e33712a8eede02762a536bdc2f89e736e0ad87bd13b35d724306585aeb54/coverage-7.11.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e37486aed7045c280ebdc207026bdef9267730177d929a5e25250e1f33cc125", size = 250524, upload-time = "2025-11-07T10:50:41.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/c9/6181877977a0f6e46b9c93a8382b8c671769fb12df8a15be8d6091541b77/coverage-7.11.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7c68180e67b4843674bfb1d3ec928ffcfc94081b5da959e616405eca51c23356", size = 248673, upload-time = "2025-11-07T10:50:43.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/d6/ff26c2eb57d4dcd46c6ed136d6b04aceb7f58f48dcc500c77f7194711a6f/coverage-7.11.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cf825b60f94d1706c22d4887310db26cc3117d545ac6ad4229b4a0d718afcf9a", size = 248251, upload-time = "2025-11-07T10:50:45.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/ff/411803f1fcb9efe00afbc96442564cc691f537541a8bde377cf1ac04e695/coverage-7.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:437149272ff0440df66044bd6ee87cbc252463754ca43cafa496cfb2f57f56dd", size = 250111, upload-time = "2025-11-07T10:50:46.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/9f/781c045e1e5f8930f8266f224318040413b60837749d2ed11883b7478c81/coverage-7.11.1-cp312-cp312-win32.whl", hash = "sha256:98ea0b8d1addfc333494c2248af367e8ecb27724a99804a18376b801f876da58", size = 219407, upload-time = "2025-11-07T10:50:48.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/59/813d8eedc96a781e8a6f9c37f6ecb4326ebbffdafe2e1154ed2def468b76/coverage-7.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d49a473799e55a465bcadd19525977ab80031b8b86baaa622241808df4585cd", size = 220220, upload-time = "2025-11-07T10:50:51.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/5f/c0905d9159d38194943a21d7d013f1c2f0c43e7d63f680ed56269728418a/coverage-7.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:0c77e5951ab176a6ccb70c6f688fca2a7ac834753ba82ee4eb741be655f30b43", size = 218856, upload-time = "2025-11-07T10:50:53.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/01/0c50c318f5e8f1a482da05d788d0ff06137803ed8fface4a1ba51e04b3ad/coverage-7.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:da9930594ca99d66eb6f613d7beba850db2f8dfa86810ee35ae24e4d5f2bb97d", size = 216920, upload-time = "2025-11-07T10:50:55.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/11/9f038e6c2baea968c377ab355b0d1d0a46b5f38985691bf51164e1b78c1f/coverage-7.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc47a280dc014220b0fc6e5f55082a3f51854faf08fd9635b8a4f341c46c77d3", size = 217301, upload-time = "2025-11-07T10:50:57.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/cd/9dcf93d81d0cddaa0bba90c3b4580e6f1ddf833918b816930d250cc553a4/coverage-7.11.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:74003324321bbf130939146886eddf92e48e616b5910215e79dea6edeb8ee7c8", size = 248277, upload-time = "2025-11-07T10:50:59.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/f5/b2c7c494046c9c783d3cac4c812fc24d6104dd36a7a598e7dd6fea3e7927/coverage-7.11.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:211f7996265daab60a8249af4ca6641b3080769cbedcffc42cc4841118f3a305", size = 250871, upload-time = "2025-11-07T10:51:01.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/5a/b359649566954498aa17d7c98093182576d9e435ceb4ea917b3b48d56f86/coverage-7.11.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70619d194d8fea0cb028cb6bb9c85b519c7509c1d1feef1eea635183bc8ecd27", size = 252115, upload-time = "2025-11-07T10:51:03.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/17/3cef1ede3739622950f0737605353b797ec564e70c9d254521b10f4b03ba/coverage-7.11.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0208bb59d441cfa3321569040f8e455f9261256e0df776c5462a1e5a9b31e13", size = 248442, upload-time = "2025-11-07T10:51:04.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/63/d5854c47ae42d9d18855329db6bc528f5b7f4f874257edb00cf8b483f9f8/coverage-7.11.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:545714d8765bda1c51f8b1c96e0b497886a054471c68211e76ef49dd1468587d", size = 250253, upload-time = "2025-11-07T10:51:06.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/e8/c7706f8a5358a59c18b489e7e19e83d6161b7c8bc60771f95920570c94a8/coverage-7.11.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d0a2b02c1e20158dd405054bcca87f91fd5b7605626aee87150819ea616edd67", size = 248217, upload-time = "2025-11-07T10:51:08.405Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/c9/a2136dfb168eb09e2f6d9d6b6c986243fdc0b3866a9376adb263d3c3378b/coverage-7.11.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0f4aa986a4308a458e0fb572faa3eb3db2ea7ce294604064b25ab32b435a468", size = 248040, upload-time = "2025-11-07T10:51:10.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/9a/a63991c0608ddc6adf65e6f43124951aaf36bd79f41937b028120b8268ea/coverage-7.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d51cc6687e8bbfd1e041f52baed0f979cd592242cf50bf18399a7e03afc82d88", size = 249801, upload-time = "2025-11-07T10:51:12.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/19/947acf7c0c6e90e4ec3abf474133ed36d94407d07e36eafdfd3acb59fee9/coverage-7.11.1-cp313-cp313-win32.whl", hash = "sha256:1b3067db3afe6deeca2b2c9f0ec23820d5f1bd152827acfadf24de145dfc5f66", size = 219430, upload-time = "2025-11-07T10:51:14.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/54/36fef7afb3884450c7b6d494fcabe2fab7c669d547c800ca30f41c1dc212/coverage-7.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:39a4c44b0cd40e3c9d89b2b7303ebd6ab9ae8a63f9e9a8c4d65a181a0b33aebe", size = 220239, upload-time = "2025-11-07T10:51:16.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/dc/7d38bb99e8e69200b7dd5de15507226bd90eac102dfc7cc891b9934cdc76/coverage-7.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:a2e3560bf82fa8169a577e054cbbc29888699526063fee26ea59ea2627fd6e73", size = 218868, upload-time = "2025-11-07T10:51:18.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/c6/d1ff54fbd6bcad42dbcfd13b417e636ef84aae194353b1ef3361700f2525/coverage-7.11.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47a4f362a10285897ab3aa7a4b37d28213a4f2626823923613d6d7a3584dd79a", size = 217615, upload-time = "2025-11-07T10:51:21.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/f9/6ed59e7cf1488d6f975e5b14ef836f5e537913523e92175135f8518a83ce/coverage-7.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0df35fa7419ef571db9dacd50b0517bc54dbfe37eb94043b5fc3540bff276acd", size = 217960, upload-time = "2025-11-07T10:51:22.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/74/2dab1dc2ebe16f074f80ae483b0f45faf278d102be703ac01b32cd85b6c3/coverage-7.11.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e1a2c621d341c9d56f7917e56fbb56be4f73fe0d0e8dae28352fb095060fd467", size = 259262, upload-time = "2025-11-07T10:51:24.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/49/eccfe039663e29a50a54b0c2c8d076acd174d7ac50d018ef8a5b1c37c8dc/coverage-7.11.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c354b111be9b2234d9573d75dd30ca4e414b7659c730e477e89be4f620b3fb5", size = 261326, upload-time = "2025-11-07T10:51:26.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/bb/2b829aa23fd5ee8318e33cc02a606eb09900921291497963adc3f06af8bb/coverage-7.11.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4589bd44698728f600233fb2881014c9b8ec86637ef454c00939e779661dbe7e", size = 263758, upload-time = "2025-11-07T10:51:27.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/03/d44c3d70e5da275caf2cad2071da6b425412fbcb1d1d5a81f1f89b45e3f1/coverage-7.11.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c6956fc8754f2309131230272a7213a483a32ecbe29e2b9316d808a28f2f8ea1", size = 258444, upload-time = "2025-11-07T10:51:30.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/c1/cf61d9f46ae088774c65dd3387a15dfbc72de90c1f6e105025e9eda19b42/coverage-7.11.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63926a97ed89dc6a087369b92dcb8b9a94cead46c08b33a7f1f4818cd8b6a3c3", size = 261335, upload-time = "2025-11-07T10:51:31.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/9a/b3299bb14f11f2364d78a2b9704491b15395e757af6116694731ce4e5834/coverage-7.11.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f5311ba00c53a7fb2b293fdc1f478b7286fe2a845a7ba9cda053f6e98178f0b4", size = 258951, upload-time = "2025-11-07T10:51:33.925Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/a3/73cb2763e59f14ba6d8d6444b1f640a9be2242bfb59b7e50581c695db7ff/coverage-7.11.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:31bf5ffad84c974f9e72ac53493350f36b6fa396109159ec704210698f12860b", size = 257840, upload-time = "2025-11-07T10:51:36.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/db/482e72589a952027e238ffa3a15f192c552e0685fd0c5220ad05b5f17d56/coverage-7.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:227ee59fbc4a8c57a7383a1d7af6ca94a78ae3beee4045f38684548a8479a65b", size = 260040, upload-time = "2025-11-07T10:51:38.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/a1/b931d3ee099c2dca8e9ea56c07ae84c0f91562f7bbbcccab8c91b3474ef1/coverage-7.11.1-cp313-cp313t-win32.whl", hash = "sha256:a447d97b3ce680bb1da2e6bd822ebb71be6a1fb77ce2c2ad2fe4bd8aacec3058", size = 220102, upload-time = "2025-11-07T10:51:40.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/53/b553b7bfa6207def4918f0cb72884c844fa4c3f1566e58fbb4f34e54cdc5/coverage-7.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d6d11180437c67bde2248563a42b8e5bbf85c8df78fae13bf818ad17bfb15f02", size = 221166, upload-time = "2025-11-07T10:51:41.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/45/1c1d58b3ed585598764bd2fe41fcf60ccafe15973ad621c322ba52e22d32/coverage-7.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:1e19a4c43d612760c6f7190411fb157e2d8a6dde00c91b941d43203bd3b17f6f", size = 219439, upload-time = "2025-11-07T10:51:43.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/32/bd9f48c28e23b2f08946f8e83983617b00619f5538dbd7e1045fa7e88c00/coverage-7.11.1-py3-none-any.whl", hash = "sha256:0fa848acb5f1da24765cee840e1afe9232ac98a8f9431c6112c15b34e880b9e8", size = 208689, upload-time = "2025-11-07T10:52:38.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/39/af056ec7a27c487e25c7f6b6e51d2ee9821dba1863173ddf4dc2eebef4f7/coverage-7.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b771b59ac0dfb7f139f70c85b42717ef400a6790abb6475ebac1ecee8de782f", size = 216676, upload-time = "2025-11-10T00:11:11.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f8/21126d34b174d037b5d01bea39077725cbb9a0da94a95c5f96929c695433/coverage-7.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:603c4414125fc9ae9000f17912dcfd3d3eb677d4e360b85206539240c96ea76e", size = 217034, upload-time = "2025-11-10T00:11:13.12Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/3f/0fd35f35658cdd11f7686303214bd5908225838f374db47f9e457c8d6df8/coverage-7.11.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:77ffb3b7704eb7b9b3298a01fe4509cef70117a52d50bcba29cffc5f53dd326a", size = 248531, upload-time = "2025-11-10T00:11:15.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/59/0bfc5900fc15ce4fd186e092451de776bef244565c840c9c026fd50857e1/coverage-7.11.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4d4ca49f5ba432b0755ebb0fc3a56be944a19a16bb33802264bbc7311622c0d1", size = 251290, upload-time = "2025-11-10T00:11:16.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/88/d5c184001fa2ac82edf1b8f2cd91894d2230d7c309e937c54c796176e35b/coverage-7.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05fd3fb6edff0c98874d752013588836f458261e5eba587afe4c547bba544afd", size = 252375, upload-time = "2025-11-10T00:11:18.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/29/f60af9f823bf62c7a00ce1ac88441b9a9a467e499493e5cc65028c8b8dd2/coverage-7.11.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e920567f8c3a3ce68ae5a42cf7c2dc4bb6cc389f18bff2235dd8c03fa405de5", size = 248946, upload-time = "2025-11-10T00:11:20.202Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/16/4662790f3b1e03fce5280cad93fd18711c35980beb3c6f28dca41b5230c6/coverage-7.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4bec8c7160688bd5a34e65c82984b25409563134d63285d8943d0599efbc448e", size = 250310, upload-time = "2025-11-10T00:11:21.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/75/dd6c2e28308a83e5fc1ee602f8204bd3aa5af685c104cb54499230cf56db/coverage-7.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:adb9b7b42c802bd8cb3927de8c1c26368ce50c8fdaa83a9d8551384d77537044", size = 248461, upload-time = "2025-11-10T00:11:23.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/fe/b71af12be9f59dc9eb060688fa19a95bf3223f56c5af1e9861dfa2275d2c/coverage-7.11.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c8f563b245b4ddb591e99f28e3cd140b85f114b38b7f95b2e42542f0603eb7d7", size = 248039, upload-time = "2025-11-10T00:11:25.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/b8/023b2003a2cd96bdf607afe03d9b96c763cab6d76e024abe4473707c4eb8/coverage-7.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e2a96fdc7643c9517a317553aca13b5cae9bad9a5f32f4654ce247ae4d321405", size = 249903, upload-time = "2025-11-10T00:11:26.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/ee/5f1076311aa67b1fa4687a724cc044346380e90ce7d94fec09fd384aa5fd/coverage-7.11.3-cp312-cp312-win32.whl", hash = "sha256:e8feeb5e8705835f0622af0fe7ff8d5cb388948454647086494d6c41ec142c2e", size = 219201, upload-time = "2025-11-10T00:11:28.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/24/d21688f48fe9fcc778956680fd5aaf69f4e23b245b7c7a4755cbd421d25b/coverage-7.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:abb903ffe46bd319d99979cdba350ae7016759bb69f47882242f7b93f3356055", size = 220012, upload-time = "2025-11-10T00:11:30.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/9e/d5eb508065f291456378aa9b16698b8417d87cb084c2b597f3beb00a8084/coverage-7.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:1451464fd855d9bd000c19b71bb7dafea9ab815741fb0bd9e813d9b671462d6f", size = 218652, upload-time = "2025-11-10T00:11:32.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/f6/d8572c058211c7d976f24dab71999a565501fb5b3cdcb59cf782f19c4acb/coverage-7.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84b892e968164b7a0498ddc5746cdf4e985700b902128421bb5cec1080a6ee36", size = 216694, upload-time = "2025-11-10T00:11:34.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/f6/b6f9764d90c0ce1bce8d995649fa307fff21f4727b8d950fa2843b7b0de5/coverage-7.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f761dbcf45e9416ec4698e1a7649248005f0064ce3523a47402d1bff4af2779e", size = 217065, upload-time = "2025-11-10T00:11:36.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/8d/a12cb424063019fd077b5be474258a0ed8369b92b6d0058e673f0a945982/coverage-7.11.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1410bac9e98afd9623f53876fae7d8a5db9f5a0ac1c9e7c5188463cb4b3212e2", size = 248062, upload-time = "2025-11-10T00:11:37.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/9c/dab1a4e8e75ce053d14259d3d7485d68528a662e286e184685ea49e71156/coverage-7.11.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:004cdcea3457c0ea3233622cd3464c1e32ebba9b41578421097402bee6461b63", size = 250657, upload-time = "2025-11-10T00:11:39.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/89/a14f256438324f33bae36f9a1a7137729bf26b0a43f5eda60b147ec7c8c7/coverage-7.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f067ada2c333609b52835ca4d4868645d3b63ac04fb2b9a658c55bba7f667d3", size = 251900, upload-time = "2025-11-10T00:11:41.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/07/75b0d476eb349f1296486b1418b44f2d8780cc8db47493de3755e5340076/coverage-7.11.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07bc7745c945a6d95676953e86ba7cebb9f11de7773951c387f4c07dc76d03f5", size = 248254, upload-time = "2025-11-10T00:11:43.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/4b/0c486581fa72873489ca092c52792d008a17954aa352809a7cbe6cf0bf07/coverage-7.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bba7e4743e37484ae17d5c3b8eb1ce78b564cb91b7ace2e2182b25f0f764cb5", size = 250041, upload-time = "2025-11-10T00:11:45.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/a3/0059dafb240ae3e3291f81b8de00e9c511d3dd41d687a227dd4b529be591/coverage-7.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbffc22d80d86fbe456af9abb17f7a7766e7b2101f7edaacc3535501691563f7", size = 248004, upload-time = "2025-11-10T00:11:46.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/93/967d9662b1eb8c7c46917dcc7e4c1875724ac3e73c3cb78e86d7a0ac719d/coverage-7.11.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0dba4da36730e384669e05b765a2c49f39514dd3012fcc0398dd66fba8d746d5", size = 247828, upload-time = "2025-11-10T00:11:48.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/1c/5077493c03215701e212767e470b794548d817dfc6247a4718832cc71fac/coverage-7.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae12fe90b00b71a71b69f513773310782ce01d5f58d2ceb2b7c595ab9d222094", size = 249588, upload-time = "2025-11-10T00:11:50.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/a5/77f64de461016e7da3e05d7d07975c89756fe672753e4cf74417fc9b9052/coverage-7.11.3-cp313-cp313-win32.whl", hash = "sha256:12d821de7408292530b0d241468b698bce18dd12ecaf45316149f53877885f8c", size = 219223, upload-time = "2025-11-10T00:11:52.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/1c/ec51a3c1a59d225b44bdd3a4d463135b3159a535c2686fac965b698524f4/coverage-7.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:6bb599052a974bb6cedfa114f9778fedfad66854107cf81397ec87cb9b8fbcf2", size = 220033, upload-time = "2025-11-10T00:11:53.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/ec/e0ce39746ed558564c16f2cc25fa95ce6fc9fa8bfb3b9e62855d4386b886/coverage-7.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:bb9d7efdb063903b3fdf77caec7b77c3066885068bdc0d44bc1b0c171033f944", size = 218661, upload-time = "2025-11-10T00:11:55.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/cb/483f130bc56cbbad2638248915d97b185374d58b19e3cc3107359715949f/coverage-7.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fb58da65e3339b3dbe266b607bb936efb983d86b00b03eb04c4ad5b442c58428", size = 217389, upload-time = "2025-11-10T00:11:57.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/ae/81f89bae3afef75553cf10e62feb57551535d16fd5859b9ee5a2a97ddd27/coverage-7.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d16bbe566e16a71d123cd66382c1315fcd520c7573652a8074a8fe281b38c6a", size = 217742, upload-time = "2025-11-10T00:11:59.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/6e/a0fb897041949888191a49c36afd5c6f5d9f5fd757e0b0cd99ec198a324b/coverage-7.11.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8258f10059b5ac837232c589a350a2df4a96406d6d5f2a09ec587cbdd539655", size = 259049, upload-time = "2025-11-10T00:12:01.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/b6/d13acc67eb402d91eb94b9bd60593411799aed09ce176ee8d8c0e39c94ca/coverage-7.11.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c5627429f7fbff4f4131cfdd6abd530734ef7761116811a707b88b7e205afd7", size = 261113, upload-time = "2025-11-10T00:12:03.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/07/a6868893c48191d60406df4356aa7f0f74e6de34ef1f03af0d49183e0fa1/coverage-7.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:465695268414e149bab754c54b0c45c8ceda73dd4a5c3ba255500da13984b16d", size = 263546, upload-time = "2025-11-10T00:12:05.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/e5/28598f70b2c1098332bac47925806353b3313511d984841111e6e760c016/coverage-7.11.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ebcddfcdfb4c614233cff6e9a3967a09484114a8b2e4f2c7a62dc83676ba13f", size = 258260, upload-time = "2025-11-10T00:12:07.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/58/58e2d9e6455a4ed746a480c4b9cf96dc3cb2a6b8f3efbee5efd33ae24b06/coverage-7.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13b2066303a1c1833c654d2af0455bb009b6e1727b3883c9964bc5c2f643c1d0", size = 261121, upload-time = "2025-11-10T00:12:09.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/57/38803eefb9b0409934cbc5a14e3978f0c85cb251d2b6f6a369067a7105a0/coverage-7.11.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d8750dd20362a1b80e3cf84f58013d4672f89663aee457ea59336df50fab6739", size = 258736, upload-time = "2025-11-10T00:12:11.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/f3/f94683167156e93677b3442be1d4ca70cb33718df32a2eea44a5898f04f6/coverage-7.11.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ab6212e62ea0e1006531a2234e209607f360d98d18d532c2fa8e403c1afbdd71", size = 257625, upload-time = "2025-11-10T00:12:12.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/ed/42d0bf1bc6bfa7d65f52299a31daaa866b4c11000855d753857fe78260ac/coverage-7.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b17c2b5e0b9bb7702449200f93e2d04cb04b1414c41424c08aa1e5d352da76", size = 259827, upload-time = "2025-11-10T00:12:15.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/76/5682719f5d5fbedb0c624c9851ef847407cae23362deb941f185f489c54e/coverage-7.11.3-cp313-cp313t-win32.whl", hash = "sha256:426559f105f644b69290ea414e154a0d320c3ad8a2bb75e62884731f69cf8e2c", size = 219897, upload-time = "2025-11-10T00:12:17.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/e0/1da511d0ac3d39e6676fa6cc5ec35320bbf1cebb9b24e9ee7548ee4e931a/coverage-7.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:90a96fcd824564eae6137ec2563bd061d49a32944858d4bdbae5c00fb10e76ac", size = 220959, upload-time = "2025-11-10T00:12:19.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/9d/e255da6a04e9ec5f7b633c54c0fdfa221a9e03550b67a9c83217de12e96c/coverage-7.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:1e33d0bebf895c7a0905fcfaff2b07ab900885fc78bba2a12291a2cfbab014cc", size = 219234, upload-time = "2025-11-10T00:12:21.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/8f/92bdd27b067204b99f396a1414d6342122f3e2663459baf787108a6b8b84/coverage-7.11.3-py3-none-any.whl", hash = "sha256:351511ae28e2509c8d8cae5311577ea7dd511ab8e746ffc8814a0896c3d33fbe", size = 208478, upload-time = "2025-11-10T00:13:14.908Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -748,15 +724,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
|
|
@ -826,11 +793,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "django-constance"
|
||||
version = "4.3.3"
|
||||
version = "4.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/c2/5a7dbd5554d840415c0f6885534385af217342fef834e6effa6a76fbee22/django_constance-4.3.3.tar.gz", hash = "sha256:6bec5f9b61730abc5b4bdb910849c7625c335e9ddce9898130735d874b63f2cb", size = 175484, upload-time = "2025-11-04T16:46:24.991Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/c7/ce27c5aac4f3283e88e4d8a5d6f8743c4952bccc56c7c474e2b08ef1ad36/django_constance-4.3.4.tar.gz", hash = "sha256:30d1b325f636177c708cce63abbde7015593f5c60b2bc0b0e2eebe8c36e2ce59", size = 175428, upload-time = "2025-11-12T10:43:01.098Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/e0/6b7ba6c3f90546741ae2f14fe7d7c7ca1d8326db2ebde8206af0136f72ff/django_constance-4.3.3-py3-none-any.whl", hash = "sha256:94f05d04d30926f92e7aaadffe4c6e0a3dccbabc657d66c42499e6141c83c1bc", size = 64425, upload-time = "2025-11-04T16:46:23.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/f0/bd5d63ad23ca97ff3445c11c6cf2dbeb52336bbecb7c4692cc9d75a24c92/django_constance-4.3.4-py3-none-any.whl", hash = "sha256:598f8c967a675fe2c7fe3688510ac8302e1b6df2141d01b8d29577532a8241db", size = 64411, upload-time = "2025-11-12T10:42:59.198Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1360,6 +1327,8 @@ dependencies = [
|
|||
{ name = "psycopg2" },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "pymdown-extensions" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-django" },
|
||||
{ name = "python-slugify" },
|
||||
{ name = "redis" },
|
||||
{ name = "requests" },
|
||||
|
|
@ -1384,14 +1353,16 @@ graph = [
|
|||
jupyter = [
|
||||
{ name = "jupyter" },
|
||||
]
|
||||
linting = [
|
||||
{ name = "celery-stubs" },
|
||||
{ name = "isort" },
|
||||
{ name = "mypy" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
openai = [
|
||||
{ name = "openai" },
|
||||
]
|
||||
testing = [
|
||||
{ name = "coverage" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-django" },
|
||||
]
|
||||
worker = [
|
||||
{ name = "celery" },
|
||||
{ name = "celery-prometheus-exporter" },
|
||||
|
|
@ -1399,52 +1370,24 @@ worker = [
|
|||
{ name = "django-celery-results" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "bandit" },
|
||||
{ name = "celery-stubs" },
|
||||
{ name = "coverage" },
|
||||
{ name = "isort" },
|
||||
{ name = "mypy" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-django" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
linting = [
|
||||
{ name = "bandit" },
|
||||
{ name = "celery-stubs" },
|
||||
{ name = "isort" },
|
||||
{ name = "mypy" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
testing = [
|
||||
{ name = "coverage" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-django" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiogram", specifier = "==3.22.0" },
|
||||
{ name = "aiosmtpd", specifier = "==1.4.6" },
|
||||
{ name = "celery", marker = "extra == 'worker'", specifier = "==5.5.3" },
|
||||
{ name = "celery-prometheus-exporter", marker = "extra == 'worker'", specifier = "==1.7.0" },
|
||||
{ name = "celery-stubs", marker = "extra == 'linting'", specifier = "==0.1.3" },
|
||||
{ name = "channels", specifier = "==4.3.1" },
|
||||
{ name = "channels-redis", specifier = ">=4.3.0" },
|
||||
{ name = "click", specifier = "==8.3.0" },
|
||||
{ name = "colorlog", specifier = "==6.10.1" },
|
||||
{ name = "coverage", specifier = "==7.11.1" },
|
||||
{ name = "coverage", marker = "extra == 'testing'", specifier = "==7.11.1" },
|
||||
{ name = "coverage", specifier = "==7.11.3" },
|
||||
{ name = "cryptography", specifier = "==46.0.3" },
|
||||
{ name = "django", specifier = "==5.2.8" },
|
||||
{ name = "django-cacheops", specifier = "==7.2" },
|
||||
{ name = "django-celery-beat", marker = "extra == 'worker'", specifier = "==2.8.1" },
|
||||
{ name = "django-celery-results", marker = "extra == 'worker'", specifier = "==2.6.0" },
|
||||
{ name = "django-constance", specifier = "==4.3.3" },
|
||||
{ name = "django-constance", specifier = "==4.3.4" },
|
||||
{ name = "django-cors-headers", specifier = "==4.9.0" },
|
||||
{ name = "django-dbbackup", specifier = "==5.0.1" },
|
||||
{ name = "django-elasticsearch-dsl", specifier = "==8.2" },
|
||||
|
|
@ -1481,8 +1424,11 @@ requires-dist = [
|
|||
{ name = "graphene-file-upload", specifier = "==1.3.0" },
|
||||
{ name = "gunicorn", specifier = "==23.0.0" },
|
||||
{ name = "httpx", specifier = "==0.28.1" },
|
||||
{ name = "isort", marker = "extra == 'linting'", specifier = "==7.0.0" },
|
||||
{ name = "jupyter", marker = "extra == 'jupyter'", specifier = "==1.1.1" },
|
||||
{ name = "m2r2", marker = "extra == 'docs'", specifier = "==0.3.4" },
|
||||
{ name = "mypy", marker = "extra == 'linting'", specifier = "==1.18.2" },
|
||||
{ name = "mypy-extensions", marker = "extra == 'linting'", specifier = "==1.1.0" },
|
||||
{ name = "openai", marker = "extra == 'openai'", specifier = "==2.6.1" },
|
||||
{ name = "paramiko", specifier = "==4.0.0" },
|
||||
{ name = "pillow", specifier = "==12.0.0" },
|
||||
|
|
@ -1492,13 +1438,14 @@ requires-dist = [
|
|||
{ name = "psycopg2", specifier = "==2.9.11" },
|
||||
{ name = "pygraphviz", marker = "sys_platform != 'win32' and extra == 'graph'", specifier = "==1.14" },
|
||||
{ name = "pyjwt", specifier = "==2.10.1" },
|
||||
{ name = "pymdown-extensions", specifier = "==10.16.1" },
|
||||
{ name = "pytest", marker = "extra == 'testing'", specifier = "==8.4.2" },
|
||||
{ name = "pytest-django", marker = "extra == 'testing'", specifier = "==4.11.1" },
|
||||
{ name = "pymdown-extensions", specifier = "==10.17.1" },
|
||||
{ name = "pytest", specifier = "==9.0.1" },
|
||||
{ name = "pytest-django", specifier = "==4.11.1" },
|
||||
{ name = "python-slugify", specifier = "==8.0.4" },
|
||||
{ name = "redis", specifier = "==7.0.1" },
|
||||
{ name = "requests", specifier = "==2.32.5" },
|
||||
{ name = "sentry-sdk", extras = ["django", "celery", "opentelemetry"], specifier = "==2.43.0" },
|
||||
{ name = "ruff", marker = "extra == 'linting'", specifier = "==0.14.4" },
|
||||
{ name = "sentry-sdk", extras = ["django", "celery", "opentelemetry"], specifier = "==2.44.0" },
|
||||
{ name = "six", specifier = "==1.17.0" },
|
||||
{ name = "sphinx", marker = "extra == 'docs'", specifier = "==8.2.3" },
|
||||
{ name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = "==3.0.2" },
|
||||
|
|
@ -1508,35 +1455,7 @@ requires-dist = [
|
|||
{ name = "whitenoise", specifier = ">=6.11.0" },
|
||||
{ name = "zeep", specifier = "==4.3.2" },
|
||||
]
|
||||
provides-extras = ["testing", "graph", "worker", "openai", "jupyter", "docs"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "bandit", specifier = "==1.8.6" },
|
||||
{ name = "celery-stubs", specifier = "==0.1.3" },
|
||||
{ name = "coverage", specifier = "==7.11.1" },
|
||||
{ name = "isort", specifier = "==7.0.0" },
|
||||
{ name = "mypy", specifier = "==1.18.2" },
|
||||
{ name = "mypy-extensions", specifier = "==1.1.0" },
|
||||
{ name = "pre-commit", specifier = "==4.3.0" },
|
||||
{ name = "pytest", specifier = "==8.4.2" },
|
||||
{ name = "pytest-django", specifier = "==4.11.1" },
|
||||
{ name = "ruff", specifier = "==0.14.4" },
|
||||
]
|
||||
linting = [
|
||||
{ name = "bandit", specifier = "==1.8.6" },
|
||||
{ name = "celery-stubs", specifier = "==0.1.3" },
|
||||
{ name = "isort", specifier = "==7.0.0" },
|
||||
{ name = "mypy", specifier = "==1.18.2" },
|
||||
{ name = "mypy-extensions", specifier = "==1.1.0" },
|
||||
{ name = "pre-commit", specifier = "==4.3.0" },
|
||||
{ name = "ruff", specifier = "==0.14.4" },
|
||||
]
|
||||
testing = [
|
||||
{ name = "coverage", specifier = "==7.11.1" },
|
||||
{ name = "pytest", specifier = "==8.4.2" },
|
||||
{ name = "pytest-django", specifier = "==4.11.1" },
|
||||
]
|
||||
provides-extras = ["graph", "worker", "linting", "openai", "jupyter", "docs"]
|
||||
|
||||
[[package]]
|
||||
name = "executing"
|
||||
|
|
@ -1763,15 +1682,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
|
|
@ -2354,18 +2264,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
|
|
@ -2419,15 +2317,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mistune"
|
||||
version = "0.8.4"
|
||||
|
|
@ -2628,15 +2517,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 = "notebook"
|
||||
version = "7.4.7"
|
||||
|
|
@ -2896,22 +2776,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/6b/99/45bb1f9926efe370c6dbe324741c749658e44cb060124f28dad201202274/polib-1.2.0-py2.py3-none-any.whl", hash = "sha256:1c77ee1b81feb31df9bca258cbc58db1bbb32d10214b173882452c73af06d62d", size = 20634, upload-time = "2023-02-23T17:53:59.919Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cfgv" },
|
||||
{ name = "identify" },
|
||||
{ name = "nodeenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "virtualenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus-client"
|
||||
version = "0.23.1"
|
||||
|
|
@ -3136,15 +3000,15 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pymdown-extensions"
|
||||
version = "10.16.1"
|
||||
version = "10.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/a987e4d549c6c82353fce5fa5f650229bb60ea4c0d1684a2714a509aef58/pymdown_extensions-10.17.1.tar.gz", hash = "sha256:60d05fe55e7fb5a1e4740fc575facad20dc6ee3a748e8d3d36ba44142e75ce03", size = 845207, upload-time = "2025-11-11T21:44:58.815Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/40/b2d7b9fdccc63e48ae4dbd363b6b89eb7ac346ea49ed667bb71f92af3021/pymdown_extensions-10.17.1-py3-none-any.whl", hash = "sha256:1f160209c82eecbb5d8a0d8f89a4d9bd6bdcbde9a8537761844cfc57ad5cd8a6", size = 266310, upload-time = "2025-11-11T21:44:56.809Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3174,7 +3038,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.2"
|
||||
version = "9.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
|
|
@ -3183,9 +3047,9 @@ dependencies = [
|
|||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3418,19 +3282,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roman-numerals-py"
|
||||
version = "3.1.0"
|
||||
|
|
@ -3529,15 +3380,15 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.43.0"
|
||||
version = "2.44.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/18/09875b4323b03ca9025bae7e6539797b27e4fc032998a466b4b9c3d24653/sentry_sdk-2.43.0.tar.gz", hash = "sha256:52ed6e251c5d2c084224d73efee56b007ef5c2d408a4a071270e82131d336e20", size = 368953, upload-time = "2025-10-29T11:26:08.156Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/62/26/ff7d93a14a0ec309021dca2fb7c62669d4f6f5654aa1baf60797a16681e0/sentry_sdk-2.44.0.tar.gz", hash = "sha256:5b1fe54dfafa332e900b07dd8f4dfe35753b64e78e7d9b1655a28fd3065e2493", size = 371464, upload-time = "2025-11-11T09:35:56.075Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/31/8228fa962f7fd8814d634e4ebece8780e2cdcfbdf0cd2e14d4a6861a7cd5/sentry_sdk-2.43.0-py2.py3-none-any.whl", hash = "sha256:4aacafcf1756ef066d359ae35030881917160ba7f6fc3ae11e0e58b09edc2d5d", size = 400997, upload-time = "2025-10-29T11:26:05.77Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/56/c16bda4d53012c71fa1b588edde603c6b455bc8206bf6de7b83388fcce75/sentry_sdk-2.44.0-py2.py3-none-any.whl", hash = "sha256:9e36a0372b881e8f92fdbff4564764ce6cec4b7f25424d0a3a8d609c9e4651a7", size = 402352, upload-time = "2025-11-11T09:35:54.1Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
|
@ -3727,15 +3578,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stevedore"
|
||||
version = "5.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/5f/8418daad5c353300b7661dd8ce2574b0410a6316a8be650a189d5c68d938/stevedore-5.5.0.tar.gz", hash = "sha256:d31496a4f4df9825e1a1e4f1f74d19abb0154aff311c3b376fcc89dae8fccd73", size = 513878, upload-time = "2025-08-25T12:54:26.806Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/80/c5/0c06759b95747882bb50abda18f5fb48c3e9b0fbfc6ebc0e23550b52415d/stevedore-5.5.0-py3-none-any.whl", hash = "sha256:18363d4d268181e8e8452e71a38cd77630f345b2ef6b4a8d5614dac5ee0d18cf", size = 49518, upload-time = "2025-08-25T12:54:25.445Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swapper"
|
||||
version = "1.4.0"
|
||||
|
|
@ -3920,20 +3762,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.35.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
{ name = "filelock" },
|
||||
{ name = "platformdirs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.14"
|
||||
|
|
|
|||
Loading…
Reference in a new issue