Add ESC data and improve scoring system functionality

This commit is contained in:
2025-06-10 22:30:08 +02:00
parent a9ab9ff75a
commit 2fd9d7f548
7 changed files with 269 additions and 113 deletions

View File

@@ -13,6 +13,7 @@ dependencies = [
[dependency-groups]
dev = [
"basedpyright>=1.29.2",
"orjson>=3.10.18",
"ruff>=0.11.13",
]
@@ -38,3 +39,13 @@ docstring-code-line-length = "dynamic"
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"PLR0915",
"D100",
"D104",
"COM812",
"D213",
"D203",
"ISC003" # conflicts with basedpyright reportImplicitStringConcatenation"
]
per-file-ignores = { "tests/**/*" = ["S101", "BLE001"] }

View File

@@ -1,22 +1,25 @@
from pathlib import Path
import time
from pathlib import Path
import typer
from rich import print
from rich.table import Table
from rich import print # noqa: A004
from rich.console import Console
from rich.prompt import Prompt, Confirm
from rich.spinner import Spinner
from rich.panel import Panel
from rich.live import Live
from rich.panel import Panel
from rich.prompt import Confirm, Prompt
from rich.spinner import Spinner
from rich.table import Table
app = typer.Typer()
def parse_jury_file(file_path: Path) -> dict[str, int]:
"""Parse the jury file and return a dictionary of scores."""
with file_path.open("r", encoding="utf-8") as file:
jury_data: dict[str, int] = {}
for line in file:
parts = line.strip().split()
if len(parts) >= 2:
if len(parts) >= 2: # noqa: PLR2004
name = " ".join(parts[:-1])
score = parts[-1]
jury_data[name] = int(score)
@@ -24,7 +27,9 @@ def parse_jury_file(file_path: Path) -> dict[str, int]:
print(f"[red]Invalid line format in [bold]{file_path}[/bold]: {line.strip()}[/red]")
return jury_data
def render_table(jury_data: dict[str, int], televoting_data: dict[str, int]) -> Table:
"""Render the jury and televoting scores table."""
table = Table("Name", "Jury Score", "Televoting Score", title="Scores")
for name in sorted(jury_data.keys()):
jury_score = jury_data.get(name, 0)
@@ -32,21 +37,28 @@ def render_table(jury_data: dict[str, int], televoting_data: dict[str, int]) ->
table.add_row(name, str(jury_score), str(televoting_score))
return table
def render_table_final(final_scores: dict[str, int], jury_data: dict[str, int], televoting_data: dict[str, int]) -> Table:
def render_table_final(
final_scores: dict[str, int], jury_data: dict[str, int], televoting_data: dict[str, int]
) -> Table:
"""Render the final scores table with jury and televoting scores."""
table = Table("Name", "Jury Score", "Televoting Score", "Final Score", title="Final Scores")
for name in final_scores.keys():
for name, score in final_scores.items():
jury_score = jury_data.get(name, 0)
televoting_score = televoting_data.get(name, 0)
final_score = final_scores[name]
final_score = score
table.add_row(name, str(jury_score), str(televoting_score), str(final_score))
return table
def sort_dict[A, B](d: dict[A, B], reverse: bool = True) -> dict[A, B]:
def sort_dict[A, B](d: dict[A, B], *, reverse: bool = True) -> dict[A, B]:
"""Sorts a dictionary by its values in descending order."""
return dict(sorted(d.items(), key=lambda item: item[1], reverse=reverse))
return dict(sorted(d.items(), key=lambda item: item[1], reverse=reverse)) # pyright: ignore [reportCallIssue, reportUnknownArgumentType, reportArgumentType]
@app.command()
def run(jury_path: Path = "jury.txt"): # pyright: ignore [reportArgumentType]
def run(jury_path: Path = "jury.txt", participating_countries: int = 37) -> None: # pyright: ignore [reportArgumentType]
"""Run the ESC jury and televoting scoring prediction system."""
jury_file = Path(jury_path)
if not jury_file.exists():
print(f"[red]File [bold]{jury_path}[bold/] does not exist.[/red]")
@@ -61,7 +73,7 @@ def run(jury_path: Path = "jury.txt"): # pyright: ignore [reportArgumentType]
table = render_table(jury_scores, televoting_data)
console.print(table)
# Collect N1 televote scores
# Collect N-1 televote scores
while len(televoting_data) < len(jury_scores) - 1:
name = list(jury_scores.keys())[len(televoting_data)]
r: str = Prompt.ask(f"Enter televoting score for [bold]{name}[/bold] (go back with b):", default="0")
@@ -86,13 +98,13 @@ def run(jury_path: Path = "jury.txt"): # pyright: ignore [reportArgumentType]
console.print(render_table(jury_scores, televoting_data))
# After N-1 entries, confirm before computing last
if len(televoting_data) == len(jury_scores) - 1:
if not Confirm.ask("Final score entered! Review and continue?", default=True):
# remove last entry if they want to re-enter
last = list(televoting_data.keys())[-1]
del televoting_data[last]
console.clear()
console.print(render_table(jury_scores, televoting_data))
if len(televoting_data) == len(jury_scores) - 1 and not Confirm.ask(
"Final score entered! Review and continue?", default=True
):
last = list(televoting_data.keys())[-1]
del televoting_data[last]
console.clear()
console.print(render_table(jury_scores, televoting_data))
console.clear()
print("[green]Televoting scores have been successfully entered![/green]")
@@ -103,8 +115,10 @@ def run(jury_path: Path = "jury.txt"): # pyright: ignore [reportArgumentType]
for _ in range(20):
time.sleep(0.1)
# Compute the 38×58 total and subtract the N-1 sum
total_available = sum((12, 10, 8, 7, 6, 5, 4, 3, 2, 1)) * (len(jury_scores) + 1) # N-1 televoting + 1 rest of the world
# Compute the (participating_countries + 1)x58 total and subtract the N-1 sum
total_available = sum((12, 10, 8, 7, 6, 5, 4, 3, 2, 1)) * (
participating_countries + 1
) # N-1 televoting + 1 rest of the world
sum_entered = sum(televoting_data.values())
missing = set(jury_scores) - set(televoting_data)
country_to_predict = missing.pop()
@@ -112,12 +126,9 @@ def run(jury_path: Path = "jury.txt"): # pyright: ignore [reportArgumentType]
# Sort and compute finals
televoting_data = sort_dict(televoting_data)
jury_scores = sort_dict(jury_scores)
final_scores: dict[str, int] = {
name: jury_scores[name] + televoting_data.get(name, 0)
for name in jury_scores
}
final_scores: dict[str, int] = {name: jury_scores[name] + televoting_data.get(name, 0) for name in jury_scores}
final_scores = dict(sorted(final_scores.items(), key=lambda item: item[1], reverse=True))
# Render and announce

0
tests/__init__.py Normal file
View File

118
tests/esc_data.json Normal file
View File

@@ -0,0 +1,118 @@
{
"2025": {
"jury": {
"Austria": 258,
"Switzerland": 214,
"France": 180,
"Italy": 159,
"Netherlands": 133,
"Sweden": 126,
"Latvia": 116,
"Greece": 105,
"Estonia": 98,
"United Kingdom": 88,
"Finland": 88,
"Malta": 83,
"Germany": 77,
"Ukraine": 60,
"Israel": 60,
"Albania": 45,
"Denmark": 45,
"Armenia": 42,
"Portugal": 37,
"Lithuania": 34,
"Spain": 27,
"Luxembourg": 23,
"Norway": 22,
"Poland": 17,
"San Marino": 9,
"Iceland": 0
},
"televote": {
"Israel": 297,
"Estonia": 258,
"Sweden": 195,
"Austria": 178,
"Albania": 173,
"Ukraine": 158,
"Poland": 139,
"Greece": 126,
"Finland": 108,
"Italy": 97,
"Germany": 74,
"Norway": 67,
"Lithuania": 62,
"France": 50,
"Latvia": 42,
"Netherlands": 42,
"Iceland": 33,
"Armenia": 30,
"Luxembourg": 24,
"San Marino": 18,
"Portugal": 13,
"Spain": 10,
"Malta": 8,
"Denmark": 2,
"United Kingdom": 0,
"Switzerland": 0
},
"winner": "Austria"
},
"2024": {
"jury": {
"Switzerland": 365,
"France": 218,
"Croatia": 210,
"Italy": 164,
"Ukraine": 146,
"Ireland": 142,
"Portugal": 139,
"Sweden": 125,
"Armenia": 101,
"Germany": 99,
"Luxembourg": 83,
"Israel": 52,
"United Kingdom": 46,
"Greece": 41,
"Latvia": 36,
"Cyprus": 34,
"Lithuania": 32,
"Serbia": 22,
"Spain": 19,
"Austria": 19,
"Georgia": 15,
"Slovenia": 15,
"Norway": 12,
"Finland": 7,
"Estonia": 4
},
"televote": {
"Croatia": 337,
"Israel": 323,
"Ukraine": 307,
"France": 227,
"Switzerland": 226,
"Ireland": 136,
"Italy": 104,
"Greece": 85,
"Armenia": 82,
"Lithuania": 58,
"Sweden": 49,
"Cyprus": 44,
"Estonia": 33,
"Serbia": 32,
"Finland": 31,
"Latvia": 28,
"Luxembourg": 20,
"Georgia": 19,
"Germany": 18,
"Portugal": 13,
"Slovenia": 12,
"Spain": 11,
"Austria": 5,
"Norway": 4,
"United Kingdom": 0
},
"winner": "Switzerland"
}
}

61
tests/test_esc.py Normal file
View File

@@ -0,0 +1,61 @@
import os
import sys
import orjson
sys.path.append(os.path.join(os.path.dirname(__file__), "..")) # noqa: PTH120, PTH118
import tempfile
from pathlib import Path
from typing import TypedDict
import pytest
from typer.testing import CliRunner
from src.__main__ import app
data_path = Path(__file__).parent / "esc_data.json"
class ESCData(TypedDict):
"""TypedDict for ESC data."""
jury: dict[str, int]
televote: dict[str, int]
winner: str
with data_path.open("r", encoding="utf-8") as f:
data = orjson.loads(f.read())
ESC_DATA: dict[int, "ESCData"] = {int(year): value for year, value in data.items()}
TADA = "🎉"
@pytest.mark.parametrize(("year", "data"), ESC_DATA.items())
def test_esc_grand_final(year: int, data: ESCData) -> None:
"""Test the ESC grand final for a given year."""
jury_scores: dict[str, int] = data["jury"]
televote_scores: dict[str, int] = data["televote"]
expected_winner: str = data["winner"]
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as f:
for country, score in jury_scores.items():
f.write(f"{country} {score}\n")
inputs: list[str] = []
for country in reversed(list(jury_scores.keys())):
inputs.append(str(televote_scores[country])) # noqa: PERF401
inputs.append("y") # to confirm the winner
runner = CliRunner()
result = runner.invoke(app, ["--jury-path", f.name], input="\n".join(inputs))
try:
actual = result.output.split(TADA)[1].strip().split()[0]
except Exception:
pytest.fail(f"Could not parse winner from output:\n{result.output}", pytrace=False)
assert actual == expected_winner, f"For {year}, expected winner {expected_winner} but got {actual!r}"

View File

@@ -1,85 +0,0 @@
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import tempfile
from typer.testing import CliRunner
from src.__main__ import app
# 2025 Eurovision Grand Final jury scores
JURY_SCORES = {
"Austria": 258,
"Switzerland": 214,
"France": 180,
"Italy": 159,
"Netherlands": 133,
"Sweden": 126,
"Latvia": 116,
"Greece": 105,
"Estonia": 98,
"United Kingdom": 88,
"Finland": 88,
"Malta": 83,
"Germany": 77,
"Ukraine": 60,
"Israel": 60,
"Albania": 45,
"Denmark": 45,
"Armenia": 42,
"Portugal": 37,
"Lithuania": 34,
"Spain": 27,
"Luxembourg": 23,
"Norway": 22,
"Poland": 17,
"San Marino": 9,
"Iceland": 0,
}
# 2025 Eurovision Grand Final televoting scores
TELEVOTE_SCORES = {
"Israel": 297,
"Estonia": 258,
"Sweden": 195,
"Austria": 178,
"Albania": 173,
"Ukraine": 158,
"Poland": 139,
"Greece": 126,
"Finland": 108,
"Italy": 97,
"Germany": 74,
"Norway": 67,
"Lithuania": 62,
"France": 50,
"Latvia": 42,
"Netherlands": 42,
"Iceland": 33,
"Armenia": 30,
"Luxembourg": 24,
"San Marino": 18,
"Portugal": 13,
"Spain": 10,
"Malta": 8,
"Denmark": 2,
"United Kingdom": 0,
"Switzerland": 0,
}
RETURN = """
╭──────────────────────────── Winner Announcement ─────────────────────────────╮
│ This year's winner is: 🎉 Austria 🎉 │
╰────────────────────────────── Congratulations! ──────────────────────────────╯
"""
def test_esc_2025():
with tempfile.NamedTemporaryFile(delete=False, mode='w', encoding='utf-8') as jury_file:
jury_file.write("\n".join(f"{country} {score}" for country, score in JURY_SCORES.items()))
runner = CliRunner()
i: str = ""
for country in reversed(list(JURY_SCORES.keys())):
i += f"{TELEVOTE_SCORES[country]}\n"
i += "y\n"
result = runner.invoke(app, ["--jury-path", jury_file.name], input=i)
assert result.stdout.strip().endswith(RETURN), f"Expected output to end with {RETURN.strip()}, got {result.output.strip()}"

40
uv.lock generated
View File

@@ -48,6 +48,7 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "basedpyright" },
{ name = "orjson" },
{ name = "ruff" },
]
@@ -61,6 +62,7 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "basedpyright", specifier = ">=1.29.2" },
{ name = "orjson", specifier = ">=3.10.18" },
{ name = "ruff", specifier = ">=0.11.13" },
]
@@ -110,6 +112,44 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/91/03/a852711aec73dfb965844592dfe226024c0da28e37d1ee54083342e38f57/nodejs_wheel_binaries-22.16.0-py2.py3-none-win_arm64.whl", hash = "sha256:2728972d336d436d39ee45988978d8b5d963509e06f063e80fe41b203ee80b28", size = 38828154 },
]
[[package]]
name = "orjson"
version = "3.10.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/1a/67236da0916c1a192d5f4ccbe10ec495367a726996ceb7614eaa687112f2/orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753", size = 249184 },
{ url = "https://files.pythonhosted.org/packages/b3/bc/c7f1db3b1d094dc0c6c83ed16b161a16c214aaa77f311118a93f647b32dc/orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17", size = 133279 },
{ url = "https://files.pythonhosted.org/packages/af/84/664657cd14cc11f0d81e80e64766c7ba5c9b7fc1ec304117878cc1b4659c/orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d", size = 136799 },
{ url = "https://files.pythonhosted.org/packages/9a/bb/f50039c5bb05a7ab024ed43ba25d0319e8722a0ac3babb0807e543349978/orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae", size = 132791 },
{ url = "https://files.pythonhosted.org/packages/93/8c/ee74709fc072c3ee219784173ddfe46f699598a1723d9d49cbc78d66df65/orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f", size = 137059 },
{ url = "https://files.pythonhosted.org/packages/6a/37/e6d3109ee004296c80426b5a62b47bcadd96a3deab7443e56507823588c5/orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c", size = 138359 },
{ url = "https://files.pythonhosted.org/packages/4f/5d/387dafae0e4691857c62bd02839a3bf3fa648eebd26185adfac58d09f207/orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad", size = 142853 },
{ url = "https://files.pythonhosted.org/packages/27/6f/875e8e282105350b9a5341c0222a13419758545ae32ad6e0fcf5f64d76aa/orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c", size = 133131 },
{ url = "https://files.pythonhosted.org/packages/48/b2/73a1f0b4790dcb1e5a45f058f4f5dcadc8a85d90137b50d6bbc6afd0ae50/orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406", size = 134834 },
{ url = "https://files.pythonhosted.org/packages/56/f5/7ed133a5525add9c14dbdf17d011dd82206ca6840811d32ac52a35935d19/orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6", size = 413368 },
{ url = "https://files.pythonhosted.org/packages/11/7c/439654221ed9c3324bbac7bdf94cf06a971206b7b62327f11a52544e4982/orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06", size = 153359 },
{ url = "https://files.pythonhosted.org/packages/48/e7/d58074fa0cc9dd29a8fa2a6c8d5deebdfd82c6cfef72b0e4277c4017563a/orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5", size = 137466 },
{ url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683 },
{ url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754 },
{ url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218 },
{ url = "https://files.pythonhosted.org/packages/04/f0/8aedb6574b68096f3be8f74c0b56d36fd94bcf47e6c7ed47a7bd1474aaa8/orjson-3.10.18-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:69c34b9441b863175cc6a01f2935de994025e773f814412030f269da4f7be147", size = 249087 },
{ url = "https://files.pythonhosted.org/packages/bc/f7/7118f965541aeac6844fcb18d6988e111ac0d349c9b80cda53583e758908/orjson-3.10.18-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1ebeda919725f9dbdb269f59bc94f861afbe2a27dce5608cdba2d92772364d1c", size = 133273 },
{ url = "https://files.pythonhosted.org/packages/fb/d9/839637cc06eaf528dd8127b36004247bf56e064501f68df9ee6fd56a88ee/orjson-3.10.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adf5f4eed520a4959d29ea80192fa626ab9a20b2ea13f8f6dc58644f6927103", size = 136779 },
{ url = "https://files.pythonhosted.org/packages/2b/6d/f226ecfef31a1f0e7d6bf9a31a0bbaf384c7cbe3fce49cc9c2acc51f902a/orjson-3.10.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7592bb48a214e18cd670974f289520f12b7aed1fa0b2e2616b8ed9e069e08595", size = 132811 },
{ url = "https://files.pythonhosted.org/packages/73/2d/371513d04143c85b681cf8f3bce743656eb5b640cb1f461dad750ac4b4d4/orjson-3.10.18-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f872bef9f042734110642b7a11937440797ace8c87527de25e0c53558b579ccc", size = 137018 },
{ url = "https://files.pythonhosted.org/packages/69/cb/a4d37a30507b7a59bdc484e4a3253c8141bf756d4e13fcc1da760a0b00cb/orjson-3.10.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0315317601149c244cb3ecef246ef5861a64824ccbcb8018d32c66a60a84ffbc", size = 138368 },
{ url = "https://files.pythonhosted.org/packages/1e/ae/cd10883c48d912d216d541eb3db8b2433415fde67f620afe6f311f5cd2ca/orjson-3.10.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0da26957e77e9e55a6c2ce2e7182a36a6f6b180ab7189315cb0995ec362e049", size = 142840 },
{ url = "https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58", size = 133135 },
{ url = "https://files.pythonhosted.org/packages/13/4a/35971fd809a8896731930a80dfff0b8ff48eeb5d8b57bb4d0d525160017f/orjson-3.10.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9e86a6af31b92299b00736c89caf63816f70a4001e750bda179e15564d7a034", size = 134810 },
{ url = "https://files.pythonhosted.org/packages/99/70/0fa9e6310cda98365629182486ff37a1c6578e34c33992df271a476ea1cd/orjson-3.10.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c382a5c0b5931a5fc5405053d36c1ce3fd561694738626c77ae0b1dfc0242ca1", size = 413491 },
{ url = "https://files.pythonhosted.org/packages/32/cb/990a0e88498babddb74fb97855ae4fbd22a82960e9b06eab5775cac435da/orjson-3.10.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8e4b2ae732431127171b875cb2668f883e1234711d3c147ffd69fe5be51a8012", size = 153277 },
{ url = "https://files.pythonhosted.org/packages/92/44/473248c3305bf782a384ed50dd8bc2d3cde1543d107138fd99b707480ca1/orjson-3.10.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d808e34ddb24fc29a4d4041dcfafbae13e129c93509b847b14432717d94b44f", size = 137367 },
{ url = "https://files.pythonhosted.org/packages/ad/fd/7f1d3edd4ffcd944a6a40e9f88af2197b619c931ac4d3cfba4798d4d3815/orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea", size = 142687 },
{ url = "https://files.pythonhosted.org/packages/4b/03/c75c6ad46be41c16f4cfe0352a2d1450546f3c09ad2c9d341110cd87b025/orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52", size = 134794 },
{ url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186 },
]
[[package]]
name = "packaging"
version = "25.0"