Test all urls
This commit is contained in:
parent
5762223616
commit
de7a7d5d0e
5 changed files with 198 additions and 136 deletions
|
|
@ -19,7 +19,14 @@ dependencies = [
|
|||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["pytest", "pytest-django", "djlint", "django-stubs", "pytest-cov"]
|
||||
dev = [
|
||||
"pytest",
|
||||
"pytest-django",
|
||||
"djlint",
|
||||
"django-stubs",
|
||||
"pytest-cov",
|
||||
"hypothesis[django]",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
DJANGO_SETTINGS_MODULE = "config.settings"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import logging
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -133,12 +134,14 @@ class Game(models.Model):
|
|||
return f"{self.display_name} ({self.name})"
|
||||
return self.display_name or self.name or self.slug or self.twitch_id
|
||||
|
||||
def get_absolute_url(self) -> str:
|
||||
"""Return canonical URL to the game details page."""
|
||||
return reverse("game_detail", kwargs={"twitch_id": self.twitch_id})
|
||||
|
||||
@property
|
||||
def organizations(self) -> models.QuerySet[Organization]:
|
||||
"""Return orgs that own games with campaigns for this game."""
|
||||
return Organization.objects.filter(
|
||||
games__drop_campaigns__game=self,
|
||||
).distinct()
|
||||
return Organization.objects.filter(games__drop_campaigns__game=self).distinct()
|
||||
|
||||
@property
|
||||
def get_game_name(self) -> str:
|
||||
|
|
|
|||
|
|
@ -3,37 +3,46 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.test import TestCase
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from hypothesis.extra.django import TestCase
|
||||
|
||||
from twitch.models import Channel
|
||||
from twitch.models import ChatBadge
|
||||
from twitch.models import ChatBadgeSet
|
||||
from twitch.models import DropBenefit
|
||||
from twitch.models import DropCampaign
|
||||
from twitch.models import Game
|
||||
from twitch.models import Organization
|
||||
from twitch.models import RewardCampaign
|
||||
from twitch.models import TimeBasedDrop
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.test.client import _MonkeyPatchedWSGIResponse
|
||||
|
||||
from twitch.tests.test_badge_views import Client
|
||||
|
||||
|
||||
class RSSFeedTestCase(TestCase):
|
||||
"""Test RSS feeds."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Set up test fixtures."""
|
||||
self.org = Organization.objects.create(
|
||||
self.org: Organization = Organization.objects.create(
|
||||
twitch_id="test-org-123",
|
||||
name="Test Organization",
|
||||
)
|
||||
self.game = Game.objects.create(
|
||||
self.game: Game = Game.objects.create(
|
||||
twitch_id="test-game-123",
|
||||
slug="test-game",
|
||||
name="Test Game",
|
||||
display_name="Test Game",
|
||||
)
|
||||
self.game.owners.add(self.org)
|
||||
self.campaign = DropCampaign.objects.create(
|
||||
self.campaign: DropCampaign = DropCampaign.objects.create(
|
||||
twitch_id="test-campaign-123",
|
||||
name="Test Campaign",
|
||||
game=self.game,
|
||||
|
|
@ -44,42 +53,42 @@ class RSSFeedTestCase(TestCase):
|
|||
|
||||
def test_organization_feed(self) -> None:
|
||||
"""Test organization feed returns 200."""
|
||||
url = reverse("twitch:organization_feed")
|
||||
response = self.client.get(url)
|
||||
url: str = reverse("twitch:organization_feed")
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/rss+xml; charset=utf-8"
|
||||
|
||||
def test_game_feed(self) -> None:
|
||||
"""Test game feed returns 200."""
|
||||
url = reverse("twitch:game_feed")
|
||||
response = self.client.get(url)
|
||||
url: str = reverse("twitch:game_feed")
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/rss+xml; charset=utf-8"
|
||||
|
||||
def test_campaign_feed(self) -> None:
|
||||
"""Test campaign feed returns 200."""
|
||||
url = reverse("twitch:campaign_feed")
|
||||
response = self.client.get(url)
|
||||
url: str = reverse("twitch:campaign_feed")
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/rss+xml; charset=utf-8"
|
||||
|
||||
def test_campaign_feed_includes_badge_description(self) -> None:
|
||||
"""Badge benefit descriptions should be visible in the RSS drop summary."""
|
||||
drop = TimeBasedDrop.objects.create(
|
||||
drop: TimeBasedDrop = TimeBasedDrop.objects.create(
|
||||
twitch_id="drop-1",
|
||||
name="Diana Chat Badge",
|
||||
campaign=self.campaign,
|
||||
required_minutes_watched=0,
|
||||
required_subs=1,
|
||||
)
|
||||
benefit = DropBenefit.objects.create(
|
||||
benefit: DropBenefit = DropBenefit.objects.create(
|
||||
twitch_id="benefit-1",
|
||||
name="Diana",
|
||||
distribution_type="BADGE",
|
||||
)
|
||||
drop.benefits.add(benefit)
|
||||
|
||||
badge_set = ChatBadgeSet.objects.create(set_id="diana")
|
||||
badge_set: ChatBadgeSet = ChatBadgeSet.objects.create(set_id="diana")
|
||||
ChatBadge.objects.create(
|
||||
badge_set=badge_set,
|
||||
badge_id="1",
|
||||
|
|
@ -90,36 +99,36 @@ class RSSFeedTestCase(TestCase):
|
|||
description="This badge was earned by subscribing.",
|
||||
)
|
||||
|
||||
url = reverse("twitch:campaign_feed")
|
||||
response = self.client.get(url)
|
||||
url: str = reverse("twitch:campaign_feed")
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode("utf-8")
|
||||
content: str = response.content.decode("utf-8")
|
||||
assert "This badge was earned by subscribing." in content
|
||||
|
||||
def test_game_campaign_feed(self) -> None:
|
||||
"""Test game-specific campaign feed returns 200."""
|
||||
url = reverse("twitch:game_campaign_feed", args=[self.game.twitch_id])
|
||||
response = self.client.get(url)
|
||||
url: str = reverse("twitch:game_campaign_feed", args=[self.game.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/rss+xml; charset=utf-8"
|
||||
# Verify the game name is in the feed
|
||||
content = response.content.decode("utf-8")
|
||||
content: str = response.content.decode("utf-8")
|
||||
assert "Test Game" in content
|
||||
|
||||
def test_organization_campaign_feed(self) -> None:
|
||||
"""Test organization-specific campaign feed returns 200."""
|
||||
url = reverse("twitch:organization_campaign_feed", args=[self.org.twitch_id])
|
||||
response = self.client.get(url)
|
||||
url: str = reverse("twitch:organization_campaign_feed", args=[self.org.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/rss+xml; charset=utf-8"
|
||||
# Verify the organization name is in the feed
|
||||
content = response.content.decode("utf-8")
|
||||
content: str = response.content.decode("utf-8")
|
||||
assert "Test Organization" in content
|
||||
|
||||
def test_game_campaign_feed_filters_correctly(self) -> None:
|
||||
"""Test game campaign feed only shows campaigns for that game."""
|
||||
# Create another game with a campaign
|
||||
other_game = Game.objects.create(
|
||||
other_game: Game = Game.objects.create(
|
||||
twitch_id="other-game-123",
|
||||
slug="other-game",
|
||||
name="Other Game",
|
||||
|
|
@ -136,9 +145,9 @@ class RSSFeedTestCase(TestCase):
|
|||
)
|
||||
|
||||
# Get feed for first game
|
||||
url = reverse("twitch:game_campaign_feed", args=[self.game.twitch_id])
|
||||
response = self.client.get(url)
|
||||
content = response.content.decode("utf-8")
|
||||
url: str = reverse("twitch:game_campaign_feed", args=[self.game.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
content: str = response.content.decode("utf-8")
|
||||
|
||||
# Should contain first campaign
|
||||
assert "Test Campaign" in content
|
||||
|
|
@ -169,11 +178,118 @@ class RSSFeedTestCase(TestCase):
|
|||
)
|
||||
|
||||
# Get feed for first organization
|
||||
url = reverse("twitch:organization_campaign_feed", args=[self.org.twitch_id])
|
||||
response = self.client.get(url)
|
||||
content = response.content.decode("utf-8")
|
||||
url: str = reverse("twitch:organization_campaign_feed", args=[self.org.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
content: str = response.content.decode("utf-8")
|
||||
|
||||
# Should contain first campaign
|
||||
assert "Test Campaign" in content
|
||||
# Should NOT contain other campaign
|
||||
assert "Other Campaign 2" not in content
|
||||
|
||||
|
||||
URL_NAMES: list[tuple[str, dict[str, str]]] = [
|
||||
("twitch:dashboard", {}),
|
||||
("twitch:badge_list", {}),
|
||||
("twitch:badge_set_detail", {"set_id": "test-set-123"}),
|
||||
("twitch:campaign_list", {}),
|
||||
("twitch:campaign_detail", {"twitch_id": "test-campaign-123"}),
|
||||
("twitch:channel_list", {}),
|
||||
("twitch:channel_detail", {"twitch_id": "test-channel-123"}),
|
||||
("twitch:debug", {}),
|
||||
("twitch:docs_rss", {}),
|
||||
("twitch:emote_gallery", {}),
|
||||
("twitch:game_list", {}),
|
||||
("twitch:game_list_simple", {}),
|
||||
("twitch:game_detail", {"twitch_id": "test-game-123"}),
|
||||
("twitch:org_list", {}),
|
||||
("twitch:organization_detail", {"twitch_id": "test-org-123"}),
|
||||
("twitch:reward_campaign_list", {}),
|
||||
("twitch:reward_campaign_detail", {"twitch_id": "test-reward-123"}),
|
||||
("twitch:search", {}),
|
||||
("twitch:campaign_feed", {}),
|
||||
("twitch:game_feed", {}),
|
||||
("twitch:game_campaign_feed", {"twitch_id": "test-game-123"}),
|
||||
("twitch:organization_feed", {}),
|
||||
("twitch:organization_campaign_feed", {"twitch_id": "test-org-123"}),
|
||||
("twitch:reward_campaign_feed", {}),
|
||||
("twitch:campaign_feed_v1", {}),
|
||||
("twitch:game_feed_v1", {}),
|
||||
("twitch:game_campaign_feed_v1", {"twitch_id": "test-game-123"}),
|
||||
("twitch:organization_feed_v1", {}),
|
||||
("twitch:organization_campaign_feed_v1", {"twitch_id": "test-org-123"}),
|
||||
("twitch:reward_campaign_feed_v1", {}),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(("url_name", "kwargs"), URL_NAMES)
|
||||
def test_rss_feeds_return_200(client: Client, url_name: str, kwargs: dict[str, str]) -> None:
|
||||
"""Test if feeds return HTTP 200.
|
||||
|
||||
Args:
|
||||
client (Client): Django test client instance.
|
||||
url_name (str): URL pattern from urls.py.
|
||||
kwargs (dict[str, str]): Extra data used in URL.
|
||||
For example 'rss/organizations/<str:twitch_id>/campaigns/' wants twitch_id.
|
||||
"""
|
||||
org: Organization = Organization.objects.create(
|
||||
twitch_id="test-org-123",
|
||||
name="Test Organization",
|
||||
)
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="test-game-123",
|
||||
slug="test-game",
|
||||
name="Test Game",
|
||||
display_name="Test Game",
|
||||
)
|
||||
game.owners.add(org)
|
||||
_campaign: DropCampaign = DropCampaign.objects.create(
|
||||
twitch_id="test-campaign-123",
|
||||
name="Test Campaign",
|
||||
game=game,
|
||||
start_at=timezone.now(),
|
||||
end_at=timezone.now() + timedelta(days=7),
|
||||
operation_names=["DropCampaignDetails"],
|
||||
)
|
||||
|
||||
_reward_campaign: RewardCampaign = RewardCampaign.objects.create(
|
||||
twitch_id="test-reward-123",
|
||||
name="Test Reward Campaign",
|
||||
brand="Test Brand",
|
||||
starts_at=timezone.now(),
|
||||
ends_at=timezone.now() + timedelta(days=14),
|
||||
status="ACTIVE",
|
||||
summary="Test reward summary",
|
||||
instructions="Watch and complete objectives",
|
||||
external_url="https://example.com/reward",
|
||||
about_url="https://example.com/about",
|
||||
is_sitewide=False,
|
||||
game=game,
|
||||
)
|
||||
|
||||
_channel: Channel = Channel.objects.create(
|
||||
twitch_id="test-channel-123",
|
||||
name="testchannel",
|
||||
display_name="TestChannel",
|
||||
)
|
||||
|
||||
badge_set: ChatBadgeSet = ChatBadgeSet.objects.create(
|
||||
set_id="test-set-123",
|
||||
)
|
||||
|
||||
_badge: ChatBadge = ChatBadge.objects.create(
|
||||
badge_set=badge_set,
|
||||
badge_id="1",
|
||||
image_url_1x="https://example.com/badge_18.png",
|
||||
image_url_2x="https://example.com/badge_36.png",
|
||||
image_url_4x="https://example.com/badge_72.png",
|
||||
title="Test Badge",
|
||||
description="Test badge description",
|
||||
click_action="visit_url",
|
||||
click_url="https://example.com",
|
||||
)
|
||||
|
||||
url: str = reverse(viewname=url_name, kwargs=kwargs)
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||
assert response.status_code == 200
|
||||
|
|
|
|||
114
twitch/urls.py
114
twitch/urls.py
|
|
@ -21,117 +21,25 @@ app_name = "twitch"
|
|||
|
||||
|
||||
rss_feeds_latest: list[URLPattern] = [
|
||||
# /rss/campaigns - RSS feed for latest drop campaigns.
|
||||
# Example:
|
||||
# https://ttvdrops.lovinator.space/rss/campaigns/
|
||||
# http://localhost:8000/rss/campaigns/
|
||||
path(
|
||||
"rss/campaigns/",
|
||||
DropCampaignFeed(),
|
||||
name="campaign_feed",
|
||||
),
|
||||
# /rss/games - RSS feed for latest games.
|
||||
# Example:
|
||||
# https://ttvdrops.lovinator.space/rss/games/
|
||||
# http://localhost:8000/rss/games/
|
||||
path(
|
||||
"rss/games/",
|
||||
GameFeed(),
|
||||
name="game_feed",
|
||||
),
|
||||
# /rss/games/<twitch_id>/campaigns/ - RSS feed for the latest drop campaigns of a specific game.
|
||||
# Example:
|
||||
# https://ttvdrops.lovinator.space/rss/games/21779/campaigns/
|
||||
# http://localhost:8000/rss/games/21779/campaigns/
|
||||
path(
|
||||
"rss/games/<str:twitch_id>/campaigns/",
|
||||
GameCampaignFeed(),
|
||||
name="game_campaign_feed",
|
||||
),
|
||||
# /rss/organizations/ - RSS feed for latest organizations.
|
||||
# Example:
|
||||
# https://ttvdrops.lovinator.space/rss/organizations/
|
||||
# http://localhost:8000/rss/organizations/
|
||||
path(
|
||||
"rss/organizations/",
|
||||
OrganizationFeed(),
|
||||
name="organization_feed",
|
||||
),
|
||||
# /rss/organizations/<str:twitch_id>/campaigns/ - RSS feed for campaigns of a specific organization.
|
||||
# Example:
|
||||
# https://ttvdrops.lovinator.space/rss/organizations/931fd934-2149-4a85-a6d8-2190aa4439f3/campaigns/
|
||||
# http://localhost:8000/rss/organizations/931fd934-2149-4a85-a6d8-2190aa4439f3/campaigns/
|
||||
path(
|
||||
"rss/organizations/<str:twitch_id>/campaigns/",
|
||||
OrganizationCampaignFeed(),
|
||||
name="organization_campaign_feed",
|
||||
),
|
||||
# /rss/reward-campaigns/ - RSS feed for campaigns of a specific organization.
|
||||
# Example:
|
||||
# https://ttvdrops.lovinator.space/rss/reward-campaigns
|
||||
# http://localhost:8000/rss/organizations/931fd934-2149-4a85-a6d8-2190aa4439f3/campaigns/
|
||||
path(
|
||||
"rss/reward-campaigns/",
|
||||
RewardCampaignFeed(),
|
||||
name="reward_campaign_feed",
|
||||
),
|
||||
path("rss/campaigns/", DropCampaignFeed(), name="campaign_feed"),
|
||||
path("rss/games/", GameFeed(), name="game_feed"),
|
||||
path("rss/games/<str:twitch_id>/campaigns/", GameCampaignFeed(), name="game_campaign_feed"),
|
||||
path("rss/organizations/", OrganizationFeed(), name="organization_feed"),
|
||||
path("rss/organizations/<str:twitch_id>/campaigns/", OrganizationCampaignFeed(), name="organization_campaign_feed"),
|
||||
path("rss/reward-campaigns/", RewardCampaignFeed(), name="reward_campaign_feed"),
|
||||
]
|
||||
|
||||
v1_rss_feeds: list[URLPattern] = [
|
||||
# /rss/v1/campaigns - RSS feed for latest drop campaigns.
|
||||
# Example:
|
||||
# https://ttvdrops.lovinator.space/rss/campaigns/
|
||||
# http://localhost:8000/rss/campaigns/
|
||||
path(
|
||||
"rss/v1/campaigns/",
|
||||
DropCampaignFeed(),
|
||||
name="campaign_feed_v1",
|
||||
),
|
||||
# /rss/v1/games - RSS feed for latest games.
|
||||
# Example:
|
||||
# https://ttvdrops.lovinator.space/rss/games/
|
||||
# http://localhost:8000/rss/games/
|
||||
path(
|
||||
"rss/v1/games/",
|
||||
GameFeed(),
|
||||
name="game_feed_v1",
|
||||
),
|
||||
# /rss/games/<twitch_id>/campaigns/ - RSS feed for the latest drop campaigns of a specific game.
|
||||
# Example:
|
||||
# https://ttvdrops.lovinator.space/rss/games/21779/campaigns/
|
||||
# http://localhost:8000/rss/games/21779/campaigns/
|
||||
path(
|
||||
"rss/v1/games/<str:twitch_id>/campaigns/",
|
||||
GameCampaignFeed(),
|
||||
name="game_campaign_feed_v1",
|
||||
),
|
||||
# /rss/organizations/ - RSS feed for latest organizations.
|
||||
# Example:
|
||||
# https://ttvdrops.lovinator.space/rss/organizations/
|
||||
# http://localhost:8000/rss/organizations/
|
||||
path(
|
||||
"rss/v1/organizations/",
|
||||
OrganizationFeed(),
|
||||
name="organization_feed_v1",
|
||||
),
|
||||
# /rss/organizations/<str:twitch_id>/campaigns/ - RSS feed for campaigns of a specific organization.
|
||||
# Example:
|
||||
# https://ttvdrops.lovinator.space/rss/organizations/931fd934-2149-4a85-a6d8-2190aa4439f3/campaigns/
|
||||
# http://localhost:8000/rss/organizations/931fd934-2149-4a85-a6d8-2190aa4439f3/campaigns/
|
||||
path("rss/v1/campaigns/", DropCampaignFeed(), name="campaign_feed_v1"),
|
||||
path("rss/v1/games/", GameFeed(), name="game_feed_v1"),
|
||||
path("rss/v1/games/<str:twitch_id>/campaigns/", GameCampaignFeed(), name="game_campaign_feed_v1"),
|
||||
path("rss/v1/organizations/", OrganizationFeed(), name="organization_feed_v1"),
|
||||
path(
|
||||
"rss/v1/organizations/<str:twitch_id>/campaigns/",
|
||||
OrganizationCampaignFeed(),
|
||||
name="organization_campaign_feed_v1",
|
||||
),
|
||||
# /rss/reward-campaigns/ - RSS feed for campaigns of a specific organization.
|
||||
# Example:
|
||||
# https://ttvdrops.lovinator.space/rss/reward-campaigns
|
||||
# http://localhost:8000/rss/organizations/931fd934-2149-4a85-a6d8-2190aa4439f3/campaigns/
|
||||
path(
|
||||
"rss/v1/reward-campaigns/",
|
||||
RewardCampaignFeed(),
|
||||
name="reward_campaign_feed_v1",
|
||||
),
|
||||
path("rss/v1/reward-campaigns/", RewardCampaignFeed(), name="reward_campaign_feed_v1"),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
28
uv.lock
generated
28
uv.lock
generated
|
|
@ -246,6 +246,23 @@ 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 = "hypothesis"
|
||||
version = "6.151.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "sortedcontainers" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/03/9fd03d5db09029250e69745c1600edab16fe90947636f77a12ba92d79939/hypothesis-6.151.4.tar.gz", hash = "sha256:658a62da1c3ccb36746ac2f7dc4bb1a6e76bd314e0dc54c4e1aaba2503d5545c", size = 475706, upload-time = "2026-01-29T01:30:14.985Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/6d/01ad1b6c3b8cb2bb47eeaa9765dabc27cbe68e3b59f6cff83d5668f57780/hypothesis-6.151.4-py3-none-any.whl", hash = "sha256:a1cf7e0fdaa296d697a68ff3c0b3912c0050f07aa37e7d2ff33a966749d1d9b4", size = 543146, upload-time = "2026-01-29T01:30:12.805Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
django = [
|
||||
{ name = "django" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
|
|
@ -541,6 +558,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sortedcontainers"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.5.5"
|
||||
|
|
@ -584,6 +610,7 @@ dependencies = [
|
|||
dev = [
|
||||
{ name = "django-stubs" },
|
||||
{ name = "djlint" },
|
||||
{ name = "hypothesis", extra = ["django"] },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-django" },
|
||||
|
|
@ -608,6 +635,7 @@ requires-dist = [
|
|||
dev = [
|
||||
{ name = "django-stubs" },
|
||||
{ name = "djlint" },
|
||||
{ name = "hypothesis", extras = ["django"] },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-django" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue