+
+
Title: Sample.Show.2026.06.19.1080p.WEB.h264-GRP
+
Metadata only.
+
+
+
+
+
Title: Sample.Show.2026.06.19.1080p.WEB.h264-GRP
+
https://ddownload.com/example
+
+
+ """
+
+ with patch(
+ "quasarr.search.sources.dl._fetch_thread_page",
+ return_value=FakeResponse(html, "https://www.source.invalid/thread.1/"),
+ ):
+ release = _date_release_from_thread(
+ FakeSharedState(),
+ "https://www.source.invalid/thread.1/",
+ "Sample Show",
+ date(2026, 6, 19),
+ )
+
+ self.assertEqual(
+ "https://www.source.invalid/thread.1/",
+ release["source"],
+ )
+
+ def test_date_release_from_thread_pins_downloadable_post(self):
+ html = """
+
+
+
Title: Sample.Show.2026.06.19.1080p.WEB.h264-GRP
+
https://ddownload.com/example
+
+
+ """
+
+ with patch(
+ "quasarr.search.sources.dl._fetch_thread_page",
+ return_value=FakeResponse(html, "https://www.source.invalid/thread.1/"),
+ ):
+ release = _date_release_from_thread(
+ FakeSharedState(),
+ "https://www.source.invalid/thread.1/",
+ "Sample Show",
+ date(2026, 6, 19),
+ )
+
+ self.assertEqual(
+ "https://www.source.invalid/thread.1/#post-2",
+ release["source"],
+ )
+
+ def test_date_release_from_thread_scans_recent_thread_pages(self):
+ first_page_html = """
+
+
+
+
Title: Sample.Show.2026.06.12.1080p.WEB.h264-GRP
+
https://ddownload.com/old-example
+
+
+
+ """
+ second_page_html = """
+
+
+
Title: Sample.Show.2026.06.19.1080p.WEB.h264-GRP
+
https://ddownload.com/example
+
+
+ """
+ fetched_thread_urls = []
+
+ def fake_fetch(_shared_state, page_url):
+ fetched_thread_urls.append(page_url)
+ if page_url.endswith("/thread.1/"):
+ return FakeResponse(first_page_html, page_url)
+ if page_url.endswith("/thread.1/page-2"):
+ return FakeResponse(second_page_html, page_url)
+ raise AssertionError(f"unexpected fetch: {page_url}")
+
+ with patch("quasarr.search.sources.dl._fetch_thread_page", fake_fetch):
+ release = _date_release_from_thread(
+ FakeSharedState(),
+ "https://www.source.invalid/thread.1/",
+ "Sample Show",
+ date(2026, 6, 19),
+ )
+
+ self.assertEqual(
+ [
+ "https://www.source.invalid/thread.1/",
+ "https://www.source.invalid/thread.1/page-2",
+ ],
+ fetched_thread_urls,
+ )
+ self.assertEqual(
+ "https://www.source.invalid/thread.1/page-2#post-2",
+ release["source"],
+ )
+
+ def test_date_release_from_thread_finds_wwe_scheduled_series_generically(self):
+ episode_date = date(2031, 2, 3)
+ cases = (
+ (
+ "WWE Monday Night RAW",
+ "WWE.RAW.2031.02.03.1080p.WEB.h264-GRP",
+ "WWE.Monday.Night.RAW.2031.02.03.1080p.WEB.h264-GRP",
+ ),
+ (
+ "WWE Friday Night SmackDown",
+ "WWE.SmackDown.2031.02.03.1080p.WEB.h264-GRP",
+ "WWE.Friday.Night.SmackDown.2031.02.03.1080p.WEB.h264-GRP",
+ ),
+ )
+
+ for search_string, posted_title, expected_title in cases:
+ with self.subTest(search_string=search_string):
+ html = f"""
+
+
+
Title: {posted_title}
+
https://ddownload.com/example
+
+
+ """
+ with patch(
+ "quasarr.search.sources.dl._fetch_thread_page",
+ return_value=FakeResponse(
+ html, "https://www.source.invalid/thread.1/"
+ ),
+ ):
+ release = _date_release_from_thread(
+ FakeSharedState(),
+ "https://www.source.invalid/thread.1/",
+ search_string,
+ episode_date,
+ )
+
+ self.assertEqual(expected_title, release["title"])
+
def test_matches_compact_ct_style_spelling(self):
current_year = datetime.now().year
diff --git a/tests/test_hostname_capabilities.py b/tests/test_hostname_capabilities.py
index ec8c2186..4e7b24d0 100644
--- a/tests/test_hostname_capabilities.py
+++ b/tests/test_hostname_capabilities.py
@@ -1,5 +1,7 @@
+import inspect
import unittest
+from quasarr.constants import SEARCH_CAT_SHOWS
from quasarr.providers.html_images import FLAG_SVGS, LANGUAGE_FLAG_EMOJI
from quasarr.search.sources import get_sources
from quasarr.search.sources.helpers import get_source_metadata
@@ -30,6 +32,48 @@ def test_flag_assets_cover_every_used_language(self):
self.assertIn(language, LANGUAGE_FLAG_EMOJI)
self.assertIn(language, FLAG_SVGS)
+ def test_date_numbering_sources_accept_shared_date_context(self):
+ for key, source in get_sources().items():
+ if (
+ SEARCH_CAT_SHOWS not in source.supported_categories
+ or not source.supports_date_numbering
+ ):
+ continue
+ with self.subTest(source=key):
+ self.assertIn(
+ "episode_date",
+ inspect.signature(source.search).parameters,
+ )
+
+ def test_date_numbering_enabled_for_all_compatible_tv_sources(self):
+ expected = {
+ "by",
+ "dd",
+ "dj",
+ "dl",
+ "dt",
+ "dw",
+ "fx",
+ "he",
+ "hs",
+ "mb",
+ "nk",
+ "nx",
+ "rm",
+ "sf",
+ "sj",
+ "sl",
+ "wd",
+ "wx",
+ }
+ actual = {
+ key
+ for key, source in get_sources().items()
+ if SEARCH_CAT_SHOWS in source.supported_categories
+ and source.supports_date_numbering
+ }
+ self.assertEqual(expected, actual)
+
class SourceMetadataTests(unittest.TestCase):
def test_metadata_exposes_expected_keys_for_every_source(self):
diff --git a/tests/test_utils_release_matching.py b/tests/test_utils_release_matching.py
index edfef14b..f104d0ff 100644
--- a/tests/test_utils_release_matching.py
+++ b/tests/test_utils_release_matching.py
@@ -1,8 +1,16 @@
# -*- coding: utf-8 -*-
import unittest
+from datetime import date
-from quasarr.providers.utils import normalize_optional_int
+from quasarr.constants import SEARCH_CAT_SHOWS
+from quasarr.providers.utils import (
+ canonicalize_date_numbered_title,
+ date_numbering_search_strings,
+ is_valid_release,
+ normalize_optional_int,
+ parse_episode_date,
+)
class ReleaseMatchingUtilsTests(unittest.TestCase):
@@ -12,6 +20,94 @@ def test_normalize_optional_int_returns_none_for_empty_string(self):
def test_normalize_optional_int_parses_numbers(self):
self.assertEqual(4, normalize_optional_int("4"))
+ def test_date_numbered_tv_release_matches_date_components(self):
+ episode_date = date(2031, 6, 19)
+ self.assertTrue(
+ is_valid_release(
+ "Sample.Show.2031.06.19.1080p.WEB.h264-GRP",
+ SEARCH_CAT_SHOWS,
+ "Sample Show",
+ season=2031,
+ episode="06/19",
+ episode_date=episode_date,
+ )
+ )
+
+ def test_date_numbered_tv_release_rejects_wrong_date(self):
+ episode_date = date(2031, 6, 19)
+ self.assertFalse(
+ is_valid_release(
+ "Sample.Show.2031.06.18.1080p.WEB.h264-GRP",
+ SEARCH_CAT_SHOWS,
+ "Sample Show",
+ season=2031,
+ episode="06/19",
+ episode_date=episode_date,
+ )
+ )
+
+ def test_date_numbered_tv_release_accepts_verified_imdb_search(self):
+ episode_date = date(2031, 6, 19)
+ self.assertTrue(
+ is_valid_release(
+ "Sample.Show.2031.06.19.1080p.WEB.h264-GRP",
+ SEARCH_CAT_SHOWS,
+ "tt0000001",
+ season=2031,
+ episode="06/19",
+ episode_date=episode_date,
+ )
+ )
+
+ def test_parse_episode_date_validates_calendar_date(self):
+ self.assertEqual(date(2031, 2, 3), parse_episode_date(2031, "02/03"))
+ self.assertIsNone(parse_episode_date(2031, "02/30"))
+ self.assertIsNone(parse_episode_date(2031, "2"))
+
+ def test_date_numbering_canonicalizes_generic_scheduled_title(self):
+ episode_date = date(2031, 2, 3)
+ self.assertEqual(
+ "Sample.Monday.Night.Showcase.2031.02.03.1080p-GRP",
+ canonicalize_date_numbered_title(
+ "Sample.Showcase.2031.02.03.1080p-GRP",
+ "Sample Monday Night Showcase",
+ episode_date,
+ ),
+ )
+
+ def test_wwe_raw_uses_generic_schedule_alias_and_canonical_title(self):
+ episode_date = date(2031, 2, 3)
+ search_strings = date_numbering_search_strings(
+ "WWE Monday Night RAW", episode_date
+ )
+
+ self.assertIn("WWE RAW 2031.02.03", search_strings)
+ self.assertEqual(
+ "WWE.Monday.Night.RAW.2031.02.03.1080p-GRP",
+ canonicalize_date_numbered_title(
+ "WWE.RAW.2031.02.03.1080p-GRP",
+ "WWE Monday Night RAW",
+ episode_date,
+ ),
+ )
+
+ def test_wwe_smackdown_uses_generic_schedule_and_case_variants(self):
+ episode_date = date(2031, 2, 3)
+ search_strings = date_numbering_search_strings(
+ "WWE Friday Night SmackDown", episode_date
+ )
+
+ self.assertIn("WWE SmackDown 2031.02.03", search_strings)
+ self.assertIn("WWE Smackdown 2031.02.03", search_strings)
+ self.assertEqual(
+ "WWE.Friday.Night.SmackDown.2031.02.03.1080p-GRP",
+ canonicalize_date_numbered_title(
+ "WWE.SmackDown.2031.02.03.1080p-GRP",
+ "WWE Friday Night SmackDown",
+ episode_date,
+ ),
+ )
+
if __name__ == "__main__":
unittest.main()
diff --git a/uv.lock b/uv.lock
index af16e639..0f5ed06b 100644
--- a/uv.lock
+++ b/uv.lock
@@ -653,27 +653,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.15.19"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d5/e6/15800dfde183a1a106594016c912b4c12d050a301989d1aca6cb63759fe8/ruff-0.15.19.tar.gz", hash = "sha256:edc27f7172a93b32b102687009d6a588508815072141543ae603a8b9b0823063", size = 4772071, upload-time = "2026-06-24T01:10:46.942Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/88/4c/9ded7626c39a0440c575bf69e2bf500d443388272c842662c59852ee7fcd/ruff-0.15.19-py3-none-linux_armv6l.whl", hash = "sha256:922d1eb283161564759bd49f507e91dc6112c15da8bd5b84ed714e086243cf86", size = 10950859, upload-time = "2026-06-24T01:10:38.491Z" },
- { url = "https://files.pythonhosted.org/packages/fb/ef/c211505ece1d00ef493d58e54e3b6383c946a21e9874774eb531f2512cf3/ruff-0.15.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4d190d8f62a0b94aba8f721116538a9ee29b1e74d26650846ba9b99f0ae21c40", size = 11294529, upload-time = "2026-06-24T01:10:36.481Z" },
- { url = "https://files.pythonhosted.org/packages/fe/93/78d462e7d39968e58094dc57be7d09ffb14ce37da5b68ed70338a35a1f21/ruff-0.15.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a2c86ba6870dd415a9d9eb8be94d7924ebec6a26ffc7958ec7ca29d4bff967d", size = 10641416, upload-time = "2026-06-24T01:10:48.923Z" },
- { url = "https://files.pythonhosted.org/packages/76/c4/5cb66cfd1f865d5cca908b86c93ac785e7f572193d3c7426079ca6643e24/ruff-0.15.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b432bc087264aea70fd25ac198918b70bd9e2aa0db4297b0bb91bbfbbc63ce", size = 11015582, upload-time = "2026-06-24T01:10:30.089Z" },
- { url = "https://files.pythonhosted.org/packages/51/9f/8ecfaec10cf5eecd28fbc00ff4fb867db90a1be54bf3d39ebf93f893cd52/ruff-0.15.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8530a09d03b3a8c994f8b559a7dcdabc690bcd3f78ef276c38c83166798ebf56", size = 10744059, upload-time = "2026-06-24T01:10:32.48Z" },
- { url = "https://files.pythonhosted.org/packages/35/6b/983249d04562bc2d590edd75f32455cdb473affb3ba4bc8d883e939c697d/ruff-0.15.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87bf21fb3875fe69f0eacc825411657e2e85589cce633c35c0adf1113649c62b", size = 11568461, upload-time = "2026-06-24T01:10:17.435Z" },
- { url = "https://files.pythonhosted.org/packages/eb/39/bc7794f127b18f492a3b4ee82bba5a900c985ff13b72b46f46e3c171ba34/ruff-0.15.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b229cb3ef56ecc2c1c8ebeca64b7a7740ccaef40a9eb097e78dde5a8560b83", size = 12429690, upload-time = "2026-06-24T01:10:40.638Z" },
- { url = "https://files.pythonhosted.org/packages/0a/3b/0de6859e698ed11c8a49e765196c8d333599b6a546c0715df39b6ba1aa2e/ruff-0.15.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c754515be7b76afe6e7e62df7776709571bcfc1631183828afcf3bafa869e3", size = 11693067, upload-time = "2026-06-24T01:10:25.681Z" },
- { url = "https://files.pythonhosted.org/packages/89/3d/0b1f30f84bee9ae6ae8d349c2ba8b6f4b040966744efdd3acc804ae7c024/ruff-0.15.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a498f82e0f4d8904c4e0aea5139cdfac1f39d19a3c51d491292f63a36e83b2e", size = 11616911, upload-time = "2026-06-24T01:10:44.809Z" },
- { url = "https://files.pythonhosted.org/packages/4d/eb/c90bd3dfc12eed9032c2c1bfe05105b93a1b2c8bce555db6308315b853ce/ruff-0.15.19-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d48caa34488fb521fd0ef4aea2b0e8fe758298df044138f0d67b687a6a0d07ed", size = 11649343, upload-time = "2026-06-24T01:10:23.472Z" },
- { url = "https://files.pythonhosted.org/packages/82/91/01caa13602a2f12fae5edbe8caf78b3c1e6db1293132aee6959eecce095c/ruff-0.15.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4171b6613effa9363cd46dd4f75bd1827b6d1b946b5e278ed0c600d305379445", size = 10977610, upload-time = "2026-06-24T01:10:50.892Z" },
- { url = "https://files.pythonhosted.org/packages/3c/51/acb817922feab9ecbb3201377d4dbe7a25f1395e46545820061973f03468/ruff-0.15.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27c15b2a241dd4d995557949a094fe78b8ad99122a38ccae1595849bcc947b3f", size = 10744900, upload-time = "2026-06-24T01:10:42.726Z" },
- { url = "https://files.pythonhosted.org/packages/84/bc/5c8ca46b8a7a3f2b16cfbec88721d772b1c93912904e8f8c2e49470fea63/ruff-0.15.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ed03b7862d68f0a8771d50ee129980cbf1b113f96e250b73954bc292f689e0bb", size = 11293560, upload-time = "2026-06-24T01:10:21.262Z" },
- { url = "https://files.pythonhosted.org/packages/81/e0/4a888cbe4d5523b3f77a2b1fa043f46cfeba1b32eac35dcfadee0578fa8a/ruff-0.15.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:08143f0685ae278b30727ea72e90c61e5bd9c31b91aac4f5bb989538f73d24b8", size = 11696533, upload-time = "2026-06-24T01:10:53.046Z" },
- { url = "https://files.pythonhosted.org/packages/98/43/c34b2fcd79262a85161764a97aaca89c3e4f574340ab61430cefa2bdd2c1/ruff-0.15.19-py3-none-win32.whl", hash = "sha256:8f47f0f92952af2557212bb10cf3e695cd4cf28b2c6e42cdb18ec6c9ebfa19da", size = 10986299, upload-time = "2026-06-24T01:10:55.185Z" },
- { url = "https://files.pythonhosted.org/packages/22/e8/15fd23e02b2442b56b2026b455977bc3057aa34b26e6323d1e99e8531a9f/ruff-0.15.19-py3-none-win_amd64.whl", hash = "sha256:efeca47ee3f9d4a7162655a3b8e6ee4a878646044233978d4d2c1ff8cdd914f0", size = 12123473, upload-time = "2026-06-24T01:10:27.74Z" },
- { url = "https://files.pythonhosted.org/packages/30/66/9a73695e31eaee04f35d8475998bf8ab354465f9c638936d76111603dcc5/ruff-0.15.19-py3-none-win_arm64.whl", hash = "sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be", size = 11376779, upload-time = "2026-06-24T01:10:34.465Z" },
+version = "0.15.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" },
+ { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" },
+ { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" },
+ { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" },
+ { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" },
+ { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" },
+ { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" },
+ { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" },
+ { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" },
]
[[package]]