diff --git a/pyproject.toml b/pyproject.toml index e71e26a..93e2f66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,3 +114,12 @@ known-first-party = ["submission_checker"] "SIM117", # deeply-nested with-patch blocks are intentional for clarity "D", # docstrings not required in test files ] + +[tool.mutmut] +also_copy = ["test_submissions/"] + +[dependency-groups] +dev = [ + "hypothesis>=6.155.3", + "mutmut>=3.6.0", +] diff --git a/src/endpoints_submission_cli/_version_check.py b/src/endpoints_submission_cli/_version_check.py new file mode 100644 index 0000000..fc8f4df --- /dev/null +++ b/src/endpoints_submission_cli/_version_check.py @@ -0,0 +1,111 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons +# SPDX-License-Identifier: Apache-2.0 +"""Background PyPI version check with a 24-hour file cache. + +Usage:: + + from endpoints_submission_cli._version_check import register_upgrade_notice + register_upgrade_notice() # call once at CLI entry + +The check never blocks the CLI: if the cache is stale a daemon thread fetches +the latest version from PyPI while the command runs, then an atexit handler +prints a one-line notice (to stderr) if a newer release exists. +""" + +from __future__ import annotations + +import atexit +import json +import threading +import time +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as _pkg_version +from pathlib import Path +from urllib.request import urlopen + +__all__ = ["register_upgrade_notice"] + +_PACKAGE = "endpoints-submission-cli" +_PYPI_URL = f"https://pypi.org/pypi/{_PACKAGE}/json" +_CACHE_PATH = Path.home() / ".cache" / _PACKAGE / "version_check.json" +_CACHE_TTL = 24 * 3600 # seconds +_TIMEOUT = 3.0 # PyPI request timeout + + +def _current_version() -> str | None: + try: + return _pkg_version(_PACKAGE) + except PackageNotFoundError: + return None + + +def _cached_latest() -> str | None: + try: + data = json.loads(_CACHE_PATH.read_text()) + if time.time() - data["ts"] < _CACHE_TTL: + return data["latest"] + except Exception: + pass + return None + + +def _fetch_latest() -> str | None: + try: + with urlopen(_PYPI_URL, timeout=_TIMEOUT) as resp: + latest: str = json.loads(resp.read())["info"]["version"] + try: + _CACHE_PATH.parent.mkdir(parents=True, exist_ok=True) + _CACHE_PATH.write_text(json.dumps({"latest": latest, "ts": time.time()})) + except Exception: + pass + return latest + except Exception: + return None + + +def _is_newer(latest: str, current: str) -> bool: + def _parts(v: str) -> tuple[int, ...]: + try: + return tuple(int(x) for x in v.split(".")[:3]) + except ValueError: + return (0,) + + return _parts(latest) > _parts(current) + + +def register_upgrade_notice() -> None: + """Register a background version check and an atexit upgrade notice. + + Safe to call multiple times — subsequent calls are no-ops. + """ + current = _current_version() + if not current: + return + + latest = _cached_latest() + result: list[str | None] = [latest] + thread: threading.Thread | None = None + + if latest is None: + + def _fetch() -> None: + result[0] = _fetch_latest() + + thread = threading.Thread(target=_fetch, daemon=True) + thread.start() + + def _on_exit() -> None: + if thread is not None: + thread.join(timeout=_TIMEOUT + 0.5) + fetched = result[0] + if fetched and _is_newer(fetched, current): + # Import here to avoid a top-level Rich import cost on every invocation. + from rich.console import Console + + Console(stderr=True).print( + f"\n[dim]A new version of [bold]{_PACKAGE}[/bold] is available: " + f"[yellow]{current}[/yellow] → [green]{fetched}[/green] " + f"([bold]pip install --upgrade {_PACKAGE}[/bold])[/dim]" + ) + + atexit.register(_on_exit) diff --git a/src/endpoints_submission_cli/main.py b/src/endpoints_submission_cli/main.py index 802bf27..6ee783e 100644 --- a/src/endpoints_submission_cli/main.py +++ b/src/endpoints_submission_cli/main.py @@ -4,6 +4,7 @@ import click +from ._version_check import register_upgrade_notice from .commands.runs import runs from .commands.submissions import submissions @@ -22,4 +23,5 @@ def app() -> None: def main() -> None: """Entry point called by the ``endpoints-submission-cli`` script.""" + register_upgrade_notice() app() diff --git a/src/submission_checker/checker.py b/src/submission_checker/checker.py index 46da3f7..14ddc25 100644 --- a/src/submission_checker/checker.py +++ b/src/submission_checker/checker.py @@ -33,6 +33,7 @@ load_accuracy_result, load_point_config, load_result_summary, + load_run_config, load_system_description, ) @@ -289,6 +290,9 @@ def _check_model( "#8.1", ) ) + else: + _, run_config_results = load_run_config(config_yaml_path) + results.extend(run_config_results) summary, load_results = load_result_summary(summary_path) results.extend(load_results) @@ -315,9 +319,12 @@ def _check_model( json_p = pd / "results.json" if not json_p.exists(): results.append( - _err("accuracy-file", - f"Missing results.json in point_{config.concurrency}/accuracy/", - json_p, "#15") + _err( + "accuracy-file", + f"Missing results.json in point_{config.concurrency}/accuracy/", + json_p, + "#15", + ) ) elif accuracy_result is None: accuracy_result, acc_results = load_accuracy_result(json_p) diff --git a/src/submission_checker/cli.py b/src/submission_checker/cli.py index 7b489e4..7a291dd 100644 --- a/src/submission_checker/cli.py +++ b/src/submission_checker/cli.py @@ -9,6 +9,13 @@ from .checker import SubmissionChecker from .models import Severity, compute_regions +try: + from endpoints_submission_cli._version_check import ( + register_upgrade_notice as _register_upgrade_notice, + ) +except ImportError: + _register_upgrade_notice = None + console = Console() _SEVERITY_STYLE: dict[Severity, str] = { @@ -22,6 +29,8 @@ @click.version_option(package_name="endpoints-submission-cli") def main() -> None: """MLPerf Endpoints submission checker — validate a submission directory.""" + if _register_upgrade_notice is not None: + _register_upgrade_notice() @main.command() diff --git a/src/submission_checker/models/__init__.py b/src/submission_checker/models/__init__.py index 453c786..17994ef 100644 --- a/src/submission_checker/models/__init__.py +++ b/src/submission_checker/models/__init__.py @@ -8,6 +8,7 @@ PercentileStats, PointConfig, PointSummary, + RunConfig, RuntimeSettings, SystemAvailabilityStatus, SystemDescription, @@ -30,6 +31,7 @@ "PointResult", "PointSummary", "RegionBounds", + "RunConfig", "Regions", "Report", "RuntimeSettings", diff --git a/src/submission_checker/models/file/__init__.py b/src/submission_checker/models/file/__init__.py index bf38df6..20b456e 100644 --- a/src/submission_checker/models/file/__init__.py +++ b/src/submission_checker/models/file/__init__.py @@ -3,6 +3,7 @@ from .accuracy import AccuracyResult from .point_config import PointConfig, RuntimeSettings from .point_summary import PercentileStats, PointSummary +from .run_config import RunConfig from .system import Division, NodeType, SystemAvailabilityStatus, SystemDescription __all__ = [ @@ -13,6 +14,7 @@ "PercentileStats", "PointConfig", "PointSummary", + "RunConfig", "RuntimeSettings", "SystemDescription", ] diff --git a/src/submission_checker/models/file/run_config.py b/src/submission_checker/models/file/run_config.py new file mode 100644 index 0000000..091ffe4 --- /dev/null +++ b/src/submission_checker/models/file/run_config.py @@ -0,0 +1,59 @@ +"""Run configuration model — ``results/point_/config.yaml`` (§8.4). + +Only the fields needed for compliance checks are parsed; all other fields are +accepted via ``extra="allow"`` so that new endpoint tool versions don't break +older checker versions. +""" + +from __future__ import annotations + +from pathlib import Path + +__all__ = ["RunConfig"] + +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, ValidationInfo, model_validator + +from ..results import CheckResult, err, ok + + +class _WarmupConfig(BaseModel): + model_config = ConfigDict(extra="allow") + + enabled: bool = False + salt: bool = False + + +class _RunSettings(BaseModel): + model_config = ConfigDict(extra="allow") + + warmup: _WarmupConfig = Field(default_factory=_WarmupConfig) + + +class RunConfig(BaseModel): + """Parsed ``results/point_/config.yaml`` — checked for warmup salt compliance.""" + + model_config = ConfigDict(extra="allow") + _check_results: list[CheckResult] = PrivateAttr(default_factory=list) + + settings: _RunSettings = Field(default_factory=_RunSettings) + + @model_validator(mode="after") + def _check_warmup_salted(self, info: ValidationInfo) -> RunConfig: + """§6.3: warmup prompts must be salted to prevent KV-cache priming of the perf run.""" + path: Path | None = (info.context or {}).get("config_path") + warmup = self.settings.warmup + if not warmup.enabled: + return self + if not warmup.salt: + self._check_results.append( + err( + "warmup-salt", + "Warmup is enabled but salt=false; unsalted prompts may prime the KV cache" + " before the performance phase", + path, + "#6.3", + ) + ) + else: + self._check_results.append(ok("warmup-salt", "Warmup salt enabled", path, "#6.3")) + return self diff --git a/src/submission_checker/models/loader.py b/src/submission_checker/models/loader.py index bcd5a94..6072b62 100644 --- a/src/submission_checker/models/loader.py +++ b/src/submission_checker/models/loader.py @@ -9,6 +9,7 @@ "load_accuracy_result", "load_point_config", "load_result_summary", + "load_run_config", "load_system_description", ] @@ -17,7 +18,7 @@ import yaml from pydantic import ValidationError -from .file import AccuracyResult, PointConfig, PointSummary, SystemDescription +from .file import AccuracyResult, PointConfig, PointSummary, RunConfig, SystemDescription from .results import CheckResult, Severity @@ -70,8 +71,14 @@ def load_system_description( """ data, load_err = _load_json(path) if load_err: - return None, [CheckResult(rule="system-description-valid", message=load_err, - severity=Severity.ERROR, path=path)] + return None, [ + CheckResult( + rule="system-description-valid", + message=load_err, + severity=Severity.ERROR, + path=path, + ) + ] try: return SystemDescription.model_validate(data), [] except ValidationError as exc: @@ -91,8 +98,11 @@ def load_point_config( """ data, load_err = _load_yaml(path) if load_err: - return None, [CheckResult(rule="point-config-valid", message=load_err, - severity=Severity.ERROR, path=path)] + return None, [ + CheckResult( + rule="point-config-valid", message=load_err, severity=Severity.ERROR, path=path + ) + ] try: instance = PointConfig.model_validate(data, context=context or {}) return instance, list(instance._check_results) @@ -110,14 +120,42 @@ def load_result_summary(path: Path) -> tuple[PointSummary | None, list[CheckResu """ data, load_err = _load_json(path) if load_err: - return None, [CheckResult(rule="result-file-valid", message=load_err, - severity=Severity.ERROR, path=path)] + return None, [ + CheckResult( + rule="result-file-valid", message=load_err, severity=Severity.ERROR, path=path + ) + ] try: return PointSummary.model_validate(data), [] except ValidationError as exc: return None, _validation_errors(exc, "result-file-valid", path) +def load_run_config( + path: Path, +) -> tuple[RunConfig | None, list[CheckResult]]: + """Load and validate ``results/point_/config.yaml``. + + Returns: + A ``(model, check_results)`` pair. On success the model is not None and + check_results contains the validator-produced CheckResult entries. + On failure the model is None and check_results contains one entry per + validation error. + """ + data, load_err = _load_yaml(path) + if load_err: + return None, [ + CheckResult( + rule="run-config-valid", message=load_err, severity=Severity.ERROR, path=path + ) + ] + try: + instance = RunConfig.model_validate(data, context={"config_path": path}) + return instance, list(instance._check_results) + except ValidationError as exc: + return None, _validation_errors(exc, "run-config-valid", path) + + def load_accuracy_result( path: Path, ) -> tuple[AccuracyResult | None, list[CheckResult]]: @@ -131,8 +169,9 @@ def load_accuracy_result( """ data, load_err = _load_json(path) if load_err: - return None, [CheckResult(rule="accuracy-valid", message=load_err, - severity=Severity.ERROR, path=path)] + return None, [ + CheckResult(rule="accuracy-valid", message=load_err, severity=Severity.ERROR, path=path) + ] try: instance = AccuracyResult.model_validate(data, context={"json_path": path}) return instance, list(instance._check_results) diff --git a/tests/endpoints_submission_cli/commands/test_common.py b/tests/endpoints_submission_cli/commands/test_common.py new file mode 100644 index 0000000..4a78cd4 --- /dev/null +++ b/tests/endpoints_submission_cli/commands/test_common.py @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons +# SPDX-License-Identifier: Apache-2.0 +"""Unit tests for endpoints_submission_cli.commands.common._run_submission_checker.""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +import pytest + +from endpoints_submission_cli.commands.common import _run_submission_checker +from endpoints_submission_cli.exceptions import SubmissionCheckError +from submission_checker.models.results import CheckResult, Report, Severity + + +def _report(tmp_path: Path, *results: CheckResult) -> Report: + return Report(submission_path=tmp_path, results=list(results)) + + +def _err(rule: str = "test-rule", message: str = "something broke") -> CheckResult: + return CheckResult(rule=rule, message=message, severity=Severity.ERROR) + + +def _ok(rule: str = "test-rule") -> CheckResult: + return CheckResult(rule=rule, message="all good", severity=Severity.INFO) + + +@pytest.mark.unit +class TestRunSubmissionChecker: + def test_raises_submission_check_error_when_errors(self, tmp_path: Path) -> None: + report = _report(tmp_path, _err("path-exists", "Submission path does not exist")) + with patch.object(Path, "cwd", return_value=tmp_path), patch( + "submission_checker.checker.SubmissionChecker" + ) as mock_cls: + mock_cls.return_value.run.return_value = report + with pytest.raises(SubmissionCheckError, match="1 error"): + _run_submission_checker(tmp_path) + + def test_no_raise_when_report_is_clean(self, tmp_path: Path) -> None: + report = _report(tmp_path, _ok("path-exists"), _ok("system-description-valid")) + with patch.object(Path, "cwd", return_value=tmp_path), patch( + "submission_checker.checker.SubmissionChecker" + ) as mock_cls: + mock_cls.return_value.run.return_value = report + _run_submission_checker(tmp_path) # must not raise + + def test_writes_log_file_to_cwd(self, tmp_path: Path) -> None: + report = _report(tmp_path) + with patch.object(Path, "cwd", return_value=tmp_path), patch( + "submission_checker.checker.SubmissionChecker" + ) as mock_cls: + mock_cls.return_value.run.return_value = report + _run_submission_checker(tmp_path) + log_files = list(tmp_path.glob("submission_checker_*.log")) + assert len(log_files) == 1 + + def test_error_message_includes_rule_and_text(self, tmp_path: Path) -> None: + report = _report(tmp_path, _err("low-throughput-coverage", "Region not covered")) + with patch.object(Path, "cwd", return_value=tmp_path), patch( + "submission_checker.checker.SubmissionChecker" + ) as mock_cls: + mock_cls.return_value.run.return_value = report + with pytest.raises(SubmissionCheckError) as exc_info: + _run_submission_checker(tmp_path) + msg = str(exc_info.value) + assert "low-throughput-coverage" in msg + assert "Region not covered" in msg + + def test_multiple_errors_all_reported(self, tmp_path: Path) -> None: + report = _report(tmp_path, _err("rule-a", "first"), _err("rule-b", "second")) + with patch.object(Path, "cwd", return_value=tmp_path), patch( + "submission_checker.checker.SubmissionChecker" + ) as mock_cls: + mock_cls.return_value.run.return_value = report + with pytest.raises(SubmissionCheckError, match="2 error"): + _run_submission_checker(tmp_path) + + def test_checker_called_with_submission_dir(self, tmp_path: Path) -> None: + report = _report(tmp_path) + with patch.object(Path, "cwd", return_value=tmp_path), patch( + "submission_checker.checker.SubmissionChecker" + ) as mock_cls: + mock_cls.return_value.run.return_value = report + _run_submission_checker(tmp_path / "my_sub") + mock_cls.assert_called_once_with(tmp_path / "my_sub") diff --git a/tests/endpoints_submission_cli/submissions/test_builder.py b/tests/endpoints_submission_cli/submissions/test_builder.py index 56b0489..2381420 100644 --- a/tests/endpoints_submission_cli/submissions/test_builder.py +++ b/tests/endpoints_submission_cli/submissions/test_builder.py @@ -5,7 +5,9 @@ from __future__ import annotations import json +import shutil import tarfile +import warnings from pathlib import Path import pytest @@ -390,3 +392,73 @@ def test_empty_string(self) -> None: def test_long_name_truncated(self) -> None: long = "A" * 100 assert len(_slugify(long)) <= 64 + + +@pytest.mark.unit +class TestSystemInfoConsistency: + """Tests for the multi-run system_info consistency check and its boundary conditions. + + The check is: if len(run_data) > 1 and not all system_infos are equal, warn. + Two mutations are targeted here: + - `not all(...)` → `all(...)` (inverted logic) + - `first = run_data[0]["system_info"]` → `first = None` (wrong reference) + """ + + def _make_archive(self, run_folder: Path, tmp_path: Path, name: str, **overrides: str) -> Path: + folder = tmp_path / name + shutil.copytree(run_folder, folder) + if overrides: + sd = json.loads((folder / "system_desc.json").read_text()) + sd.update(overrides) + (folder / "system_desc.json").write_text(json.dumps(sd)) + archive = tmp_path / f"{name}.tar.gz" + with tarfile.open(archive, "w:gz") as tar: + tar.add(folder, arcname=name) + return archive + + def test_inconsistent_system_info_warns(self, run_folder: Path, tmp_path: Path) -> None: + a1 = self._make_archive(run_folder, tmp_path, "run1") + a2 = self._make_archive( + run_folder, tmp_path, "run2", system_name="Completely Different System" + ) + with pytest.warns(UserWarning, match="inconsistent system_info"): + build_submission_folder( + [("run-001", a1), ("run-002", a2)], + "standardized", + "available", + tmp_path / "sub", + ) + + def test_consistent_system_info_no_warning(self, run_folder: Path, tmp_path: Path) -> None: + a1 = self._make_archive(run_folder, tmp_path, "run1") + # Identical system_desc, only concurrency differs (not part of system_info). + folder2 = tmp_path / "run2" + shutil.copytree(run_folder, folder2) + cfg = yaml.safe_load((folder2 / "config.yaml").read_text()) + cfg["settings"]["load_pattern"]["target_concurrency"] = 16 + (folder2 / "config.yaml").write_text(yaml.dump(cfg)) + a2 = tmp_path / "run2.tar.gz" + with tarfile.open(a2, "w:gz") as tar: + tar.add(folder2, arcname="run2") + + caught: list[warnings.WarningMessage] = [] + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + build_submission_folder( + [("run-001", a1), ("run-002", a2)], + "standardized", + "available", + tmp_path / "sub", + ) + consistency_warns = [w for w in caught if "inconsistent system_info" in str(w.message)] + assert len(consistency_warns) == 0 + + def test_single_run_no_consistency_check(self, run_archive: Path, tmp_path: Path) -> None: + # len(run_data) == 1: the > 1 guard must prevent the check from running at all. + # The > 1 → >= 1 mutation would enter the block but produce no warning (vacuous + # all() over an empty slice), so we verify the output is structurally correct + # rather than the warning count here — covered by the two tests above. + sub_dir = build_submission_folder( + [("run-001", run_archive)], "standardized", "available", tmp_path + ) + assert sub_dir.is_dir() diff --git a/tests/submission_checker/test_checks_aggregate.py b/tests/submission_checker/test_checks_aggregate.py index 91d917a..50623c5 100644 --- a/tests/submission_checker/test_checks_aggregate.py +++ b/tests/submission_checker/test_checks_aggregate.py @@ -4,7 +4,11 @@ from __future__ import annotations +from pathlib import Path + import pytest +from hypothesis import assume, given +from hypothesis import strategies as st from submission_checker.models import ( MIN_QUERY_COUNT, @@ -182,7 +186,11 @@ def test_negative_output_tokens_errors(self, tmp_path): class TestTpsConsistencyValidator: def test_system_tps_derivable_ok(self, tmp_path): run_result = PointResult.model_validate( - {"config": _config(concurrency=64), "summary": _summary(), "yaml_path": tmp_path / "run_64.yaml"}, + { + "config": _config(concurrency=64), + "summary": _summary(), + "yaml_path": tmp_path / "run_64.yaml", + }, context={"summary_path": tmp_path / "summary.json"}, ) assert any( @@ -222,7 +230,11 @@ def test_system_tps_stored_mismatch_errors(self, tmp_path): def test_tps_per_user_derivable_ok(self, tmp_path): run_result = PointResult.model_validate( - {"config": _config(concurrency=64), "summary": _summary(), "yaml_path": tmp_path / "run_64.yaml"}, + { + "config": _config(concurrency=64), + "summary": _summary(), + "yaml_path": tmp_path / "run_64.yaml", + }, context={"summary_path": tmp_path / "summary.json"}, ) assert any( @@ -328,14 +340,30 @@ def test_dataset_a_boundary(self, tmp_path): ctx = {"summary_path": tmp_path / "summary.json"} fail = PointResult.model_validate( - {"config": _config_with_dataset("dataset-a"), "summary": _summary(n_completed=0), **base}, context=ctx + { + "config": _config_with_dataset("dataset-a"), + "summary": _summary(n_completed=0), + **base, + }, + context=ctx, + ) + assert any( + r.rule == "min-query-count" and r.severity == Severity.ERROR + for r in fail._check_results ) - assert any(r.rule == "min-query-count" and r.severity == Severity.ERROR for r in fail._check_results) ok_result = PointResult.model_validate( - {"config": _config_with_dataset("dataset-a"), "summary": _summary(n_completed=1), **base}, context=ctx + { + "config": _config_with_dataset("dataset-a"), + "summary": _summary(n_completed=1), + **base, + }, + context=ctx, + ) + assert any( + r.rule == "min-query-count" and r.severity != Severity.ERROR + for r in ok_result._check_results ) - assert any(r.rule == "min-query-count" and r.severity != Severity.ERROR for r in ok_result._check_results) def test_dataset_c_boundary(self, tmp_path): """dataset-c requires 100 queries — 99 fails, 100 passes.""" @@ -343,14 +371,30 @@ def test_dataset_c_boundary(self, tmp_path): ctx = {"summary_path": tmp_path / "summary.json"} fail = PointResult.model_validate( - {"config": _config_with_dataset("dataset-c"), "summary": _summary(n_completed=99), **base}, context=ctx + { + "config": _config_with_dataset("dataset-c"), + "summary": _summary(n_completed=99), + **base, + }, + context=ctx, + ) + assert any( + r.rule == "min-query-count" and r.severity == Severity.ERROR + for r in fail._check_results ) - assert any(r.rule == "min-query-count" and r.severity == Severity.ERROR for r in fail._check_results) ok_result = PointResult.model_validate( - {"config": _config_with_dataset("dataset-c"), "summary": _summary(n_completed=100), **base}, context=ctx + { + "config": _config_with_dataset("dataset-c"), + "summary": _summary(n_completed=100), + **base, + }, + context=ctx, + ) + assert any( + r.rule == "min-query-count" and r.severity != Severity.ERROR + for r in ok_result._check_results ) - assert any(r.rule == "min-query-count" and r.severity != Severity.ERROR for r in ok_result._check_results) # --------------------------------------------------------------------------- @@ -368,7 +412,9 @@ def test_too_few(self, tmp_path): def test_cap_exceeded(self, tmp_path): ctx = _model_ctx(tmp_path, all_point_count=33) - assert any(r.rule == "point-cap" and r.severity == Severity.ERROR for r in ctx._check_results) + assert any( + r.rule == "point-cap" and r.severity == Severity.ERROR for r in ctx._check_results + ) def test_valid_count(self, tmp_path): ctx = _model_ctx(tmp_path, all_point_count=10) @@ -444,15 +490,18 @@ def test_unknown_model_emits_warning(self, tmp_path): ar = AccuracyResult({"ds": {"score": {"rouge1": "39.0"}}}) ctx = _model_ctx(tmp_path, accuracy_result=ar) assert any( - r.rule == "accuracy-gate" and r.severity == Severity.WARNING - for r in ctx._check_results + r.rule == "accuracy-gate" and r.severity == Severity.WARNING for r in ctx._check_results ) def test_passed_accuracy_gate(self, tmp_path): # rouge1 = 39.0 > threshold 38.3914 for llama3.1-8b - ar = AccuracyResult({"cnn_dailymail::llama3_8b": { - "score": {"rouge1": "39.0", "rouge2": "16.0", "rougeL": "25.0"} - }}) + ar = AccuracyResult( + { + "cnn_dailymail::llama3_8b": { + "score": {"rouge1": "39.0", "rouge2": "16.0", "rougeL": "25.0"} + } + } + ) ctx = _model_ctx(tmp_path, accuracy_result=ar, model_name="Llama-3_1-8B-Instruct") assert any( r.rule == "accuracy-gate" and r.severity == Severity.INFO for r in ctx._check_results @@ -463,9 +512,13 @@ def test_passed_accuracy_gate(self, tmp_path): def test_failed_accuracy_gate(self, tmp_path): # rouge1 = 30.0 < threshold 38.3914 for llama3.1-8b - ar = AccuracyResult({"cnn_dailymail::llama3_8b": { - "score": {"rouge1": "30.0", "rouge2": "12.0", "rougeL": "20.0"} - }}) + ar = AccuracyResult( + { + "cnn_dailymail::llama3_8b": { + "score": {"rouge1": "30.0", "rouge2": "12.0", "rougeL": "20.0"} + } + } + ) ctx = _model_ctx(tmp_path, accuracy_result=ar, model_name="Llama-3_1-8B-Instruct") assert any( r.rule == "accuracy-gate" and r.severity == Severity.ERROR for r in ctx._check_results @@ -473,10 +526,14 @@ def test_failed_accuracy_gate(self, tmp_path): def test_sample_count_passes(self, tmp_path): # 13368 == min_queries for llama3.1-8b → ok - ar = AccuracyResult({"cnn_dailymail::llama3_8b": { - "num_samples": 13368, - "score": {"rouge1": "39.0"}, - }}) + ar = AccuracyResult( + { + "cnn_dailymail::llama3_8b": { + "num_samples": 13368, + "score": {"rouge1": "39.0"}, + } + } + ) ctx = _model_ctx(tmp_path, accuracy_result=ar, model_name="Llama-3_1-8B-Instruct") assert any( r.rule == "accuracy-sample-count" and r.severity != Severity.ERROR @@ -489,10 +546,14 @@ def test_sample_count_passes(self, tmp_path): def test_sample_count_fails(self, tmp_path): # 1000 < 13368 min_queries for llama3.1-8b → error - ar = AccuracyResult({"cnn_dailymail::llama3_8b": { - "num_samples": 1000, - "score": {"rouge1": "39.0"}, - }}) + ar = AccuracyResult( + { + "cnn_dailymail::llama3_8b": { + "num_samples": 1000, + "score": {"rouge1": "39.0"}, + } + } + ) ctx = _model_ctx(tmp_path, accuracy_result=ar, model_name="Llama-3_1-8B-Instruct") assert any( r.rule == "accuracy-sample-count" and r.severity == Severity.ERROR @@ -501,8 +562,231 @@ def test_sample_count_fails(self, tmp_path): def test_sample_count_missing_skips(self, tmp_path): # no num_samples field → no accuracy-sample-count check - ar = AccuracyResult({"cnn_dailymail::llama3_8b": { - "score": {"rouge1": "39.0"}, - }}) + ar = AccuracyResult( + { + "cnn_dailymail::llama3_8b": { + "score": {"rouge1": "39.0"}, + } + } + ) ctx = _model_ctx(tmp_path, accuracy_result=ar, model_name="Llama-3_1-8B-Instruct") assert not any(r.rule == "accuracy-sample-count" for r in ctx._check_results) + + +# --------------------------------------------------------------------------- +# Property-based tests — metric-consistency invariants +# --------------------------------------------------------------------------- + + +_FAKE_PATH = Path("/fake/point.yaml") + + +def _make_result( + *, + duration_ns=1_200_000_000_000.0, + n_completed=1000, + n_issued=1000, + n_failed=0, + total_tokens=500_000.0, + concurrency=64, + **extra_fields, +): + """Build a PointResult with the given parameters, injecting any extra_fields + into PointSummary's model_extra (e.g. system_tps=, tps_per_user=). + + Uses a static fake path so the helper can be called from Hypothesis tests + without needing a function-scoped tmp_path fixture. + """ + summary = PointSummary( + n_samples_completed=n_completed, + n_samples_issued=n_issued, + n_samples_failed=n_failed, + duration_ns=duration_ns, + output_sequence_lengths=PercentileStats(total=float(total_tokens)), + **extra_fields, + ) + config = PointConfig( + concurrency=concurrency, + dataset="mlperf-perf-dataset-v1", + runtime_settings=RuntimeSettings(min_duration_ms=1_200_000), + ) + return PointResult.model_validate( + {"config": config, "summary": summary, "yaml_path": _FAKE_PATH}, + context={"summary_path": _FAKE_PATH.parent / "s.json"}, + ) + + +@pytest.mark.unit +class TestMetricConsistencyProperties: + """Property-based tests covering the boundary mutations that unit tests miss. + + Each test encodes an invariant that the implementation must satisfy for ALL + inputs in the given domain, not just the specific examples in the unit tests + above. A mutation that tweaks a boundary or flips a sign will violate at + least one of these invariants and be caught by Hypothesis's shrinker. + """ + + # ------------------------------------------------------------------ duration + + @given( + duration_ns=st.floats(min_value=1.0, max_value=1e18, allow_nan=False, allow_infinity=False) + ) + def test_positive_duration_always_emits_ok(self, duration_ns): + result = _make_result(duration_ns=duration_ns) + assert any( + r.rule == "metric-consistency-duration" and r.severity != Severity.ERROR + for r in result._check_results + ) + + @given(duration_ns=st.floats(max_value=0.0, allow_nan=False, allow_infinity=False)) + def test_nonpositive_duration_always_errors(self, duration_ns): + result = _make_result(duration_ns=duration_ns) + assert any( + r.rule == "metric-consistency-duration" and r.severity == Severity.ERROR + for r in result._check_results + ) + + # ------------------------------------------------------------------ sample accounting + + @given( + n_completed=st.integers(min_value=0, max_value=10_000), + n_failed=st.integers(min_value=1, max_value=1_000), + ) + def test_correct_accounting_with_failures_always_passes(self, n_completed, n_failed): + # n_issued set to the arithmetically correct value; n_failed > 0 so that + # the + → - mutation produces a wrong accounted total and errors. + n_issued = n_completed + n_failed + result = _make_result(n_completed=n_completed, n_issued=n_issued, n_failed=n_failed) + assert any( + r.rule == "metric-consistency-accounting" and r.severity != Severity.ERROR + for r in result._check_results + ) + + @given( + n_completed=st.integers(min_value=0, max_value=10_000), + n_failed=st.integers(min_value=0, max_value=1_000), + n_issued=st.integers(min_value=1, max_value=20_000), + ) + def test_wrong_accounting_always_errors(self, n_completed, n_failed, n_issued): + assume(n_completed + n_failed != n_issued) + result = _make_result(n_completed=n_completed, n_issued=n_issued, n_failed=n_failed) + assert any( + r.rule == "metric-consistency-accounting" and r.severity == Severity.ERROR + for r in result._check_results + ) + + def test_issued_zero_skips_accounting_check(self): + # n_samples_issued=0 means the tool didn't track dispatch count — skip the check. + # The > 0 → >= 0 mutation would run the check and emit a spurious ok result. + result = _make_result(n_completed=0, n_issued=0, n_failed=0) + assert not any(r.rule == "metric-consistency-accounting" for r in result._check_results) + + def test_issued_one_runs_accounting_check(self): + # n_samples_issued=1 with correct accounting must emit an ok result. + # The > 0 → > 1 mutation would skip the check entirely. + result = _make_result(n_completed=1, n_issued=1, n_failed=0) + assert any( + r.rule == "metric-consistency-accounting" and r.severity != Severity.ERROR + for r in result._check_results + ) + + # ------------------------------------------------------------------ output tokens + + @given(total_tokens=st.integers(min_value=0, max_value=10**8)) + def test_nonnegative_tokens_always_emits_ok(self, total_tokens): + result = _make_result(total_tokens=float(total_tokens)) + assert any( + r.rule == "metric-consistency-output-tokens" and r.severity != Severity.ERROR + for r in result._check_results + ) + + @given(total_tokens=st.integers(max_value=-1)) + def test_negative_tokens_always_errors(self, total_tokens): + result = _make_result(total_tokens=float(total_tokens)) + assert any( + r.rule == "metric-consistency-output-tokens" and r.severity == Severity.ERROR + for r in result._check_results + ) + + def test_zero_tokens_passes(self): + # total_output_tokens=0 must be ok; the < 0 → <= 0 mutation would reject it. + result = _make_result(total_tokens=0.0) + assert any( + r.rule == "metric-consistency-output-tokens" and r.severity != Severity.ERROR + for r in result._check_results + ) + + def test_duration_one_ns_passes(self): + # duration_ns=1 must be ok; the <= 0 → <= 1 mutation would reject it. + result = _make_result(duration_ns=1.0) + assert any( + r.rule == "metric-consistency-duration" and r.severity != Severity.ERROR + for r in result._check_results + ) + + +@pytest.mark.unit +class TestTpsConsistencyProperties: + """Property-based and targeted tests for the TPS formula and concurrency guard.""" + + # ------------------------------------------------------------------ concurrency guard + + @given(concurrency=st.integers(min_value=1, max_value=1000)) + def test_positive_concurrency_no_guard_error(self, concurrency): + result = _make_result(concurrency=concurrency) + assert not any( + r.rule == "metric-consistency-tps-per-user" + and r.severity == Severity.ERROR + and "not positive" in r.message + for r in result._check_results + ) + + @given(concurrency=st.integers(max_value=0)) + def test_nonpositive_concurrency_always_errors(self, concurrency): + result = _make_result(concurrency=concurrency) + assert any( + r.rule == "metric-consistency-tps-per-user" and r.severity == Severity.ERROR + for r in result._check_results + ) + + def test_concurrency_one_ok(self): + # concurrency=1 must pass; the <= 0 → <= 1 mutation would reject it. + result = _make_result(concurrency=1) + assert not any( + r.rule == "metric-consistency-tps-per-user" + and r.severity == Severity.ERROR + and "not positive" in r.message + for r in result._check_results + ) + + # ------------------------------------------------------------------ TPS formula: / vs * + + def test_stored_tps_per_user_within_tolerance_of_large_derived_passes(self): + # derived ≈ 500_000/1200/64 ≈ 6.51. stored=6.45 is ~0.9% off (within 1% tolerance). + # With the / → * mutation: rel_err = 0.06 * 6.51 ≈ 0.39 > 0.01 → would error. + result = _make_result(tps_per_user=6.45) + assert any( + r.rule == "metric-consistency-tps-per-user" and r.severity != Severity.ERROR + for r in result._check_results + ) + + def test_stored_system_tps_within_tolerance_of_large_derived_passes(self): + # derived ≈ 500_000/1200 ≈ 416.67. stored=418.75 is ~0.5% off (within 1% tolerance). + # With the / → * mutation: rel_err = 2.08 * 416.67 ≈ 867 > 0.01 → would error. + result = _make_result(system_tps=418.75) + assert any( + r.rule == "metric-consistency-system-tps" and r.severity != Severity.ERROR + for r in result._check_results + ) + + # ------------------------------------------------------------------ TPS epsilon: 1e-9 vs 1.0 + + def test_small_derived_tps_per_user_large_relative_error_errors(self): + # total_tokens=1, duration=1200s, concurrency=2 → derived ≈ 4.2e-4 (< 1.0). + # stored=0.01 is ~24× derived; rel_err ≈ 23 >> 0.01 with correct 1e-9 epsilon. + # With the 1e-9 → 1.0 epsilon mutation: denominator becomes 1.0, rel_err ≈ 0.01 - 4.2e-4 ≈ 0.0096 < 0.01 → passes (wrong). + result = _make_result(total_tokens=1.0, concurrency=2, tps_per_user=0.01) + assert any( + r.rule == "metric-consistency-tps-per-user" and r.severity == Severity.ERROR + for r in result._check_results + ) diff --git a/tests/submission_checker/test_checks_file.py b/tests/submission_checker/test_checks_file.py index ea2ba58..c784eca 100644 --- a/tests/submission_checker/test_checks_file.py +++ b/tests/submission_checker/test_checks_file.py @@ -9,6 +9,7 @@ from submission_checker.models import ( AccuracyResult, PointConfig, + RunConfig, Severity, ) @@ -55,7 +56,10 @@ def test_valid(self, tmp_path): class TestStreamingValidator: def test_stream_false_errors(self, tmp_path): config = PointConfig.model_validate( - {"concurrency": 64, "runtime_settings": {"load_pattern": "concurrency", "stream_all_chunks": False}}, + { + "concurrency": 64, + "runtime_settings": {"load_pattern": "concurrency", "stream_all_chunks": False}, + }, context={"yaml_path": tmp_path / "point_64.yaml"}, ) errors = [ @@ -125,7 +129,11 @@ def test_absent_region_no_check(self, tmp_path): def test_invalid_region_value_errors(self, tmp_path): """An unrecognised region string must produce a region-declared error.""" config = PointConfig.model_validate( - {"concurrency": 64, "region": "not_a_region", "runtime_settings": {"load_pattern": "concurrency"}}, + { + "concurrency": 64, + "region": "not_a_region", + "runtime_settings": {"load_pattern": "concurrency"}, + }, context={"yaml_path": tmp_path / "point_64.yaml"}, ) assert any( @@ -137,7 +145,11 @@ def test_valid_region_matches_computed(self, tmp_path): """Declared region matching the computed region produces an ok result.""" # concurrency=64 → med_throughput for M=1024 config = PointConfig.model_validate( - {"concurrency": 64, "region": "med_throughput", "runtime_settings": {"load_pattern": "concurrency"}}, + { + "concurrency": 64, + "region": "med_throughput", + "runtime_settings": {"load_pattern": "concurrency"}, + }, context={"yaml_path": tmp_path / "point_64.yaml", "regions": _REGIONS}, ) assert any( @@ -149,7 +161,11 @@ def test_region_mismatch_warns(self, tmp_path): """Declared region that doesn't match the computed region produces a warning.""" # concurrency=64 → med_throughput, but we declare low_latency config = PointConfig.model_validate( - {"concurrency": 64, "region": "low_latency", "runtime_settings": {"load_pattern": "concurrency"}}, + { + "concurrency": 64, + "region": "low_latency", + "runtime_settings": {"load_pattern": "concurrency"}, + }, context={"yaml_path": tmp_path / "point_64.yaml", "regions": _REGIONS}, ) assert any( @@ -160,7 +176,11 @@ def test_region_mismatch_warns(self, tmp_path): def test_submitters_choice_no_cross_check(self, tmp_path): """submitters_choice is valid for any concurrency — no cross-check performed.""" config = PointConfig.model_validate( - {"concurrency": 64, "region": "submitters_choice", "runtime_settings": {"load_pattern": "concurrency"}}, + { + "concurrency": 64, + "region": "submitters_choice", + "runtime_settings": {"load_pattern": "concurrency"}, + }, context={"yaml_path": tmp_path / "point_64.yaml", "regions": _REGIONS}, ) assert any( @@ -175,7 +195,11 @@ def test_submitters_choice_no_cross_check(self, tmp_path): def test_valid_region_no_regions_context(self, tmp_path): """Valid region string without regions context emits ok without cross-check.""" config = PointConfig.model_validate( - {"concurrency": 64, "region": "high_throughput", "runtime_settings": {"load_pattern": "concurrency"}}, + { + "concurrency": 64, + "region": "high_throughput", + "runtime_settings": {"load_pattern": "concurrency"}, + }, context={"yaml_path": tmp_path / "point_64.yaml"}, ) assert any( @@ -184,12 +208,44 @@ def test_valid_region_no_regions_context(self, tmp_path): ) +@pytest.mark.unit +class TestWarmupSaltValidator: + def _run_config(self, enabled: bool, salt: bool, tmp_path): + return RunConfig.model_validate( + {"settings": {"warmup": {"enabled": enabled, "salt": salt}}}, + context={"config_path": tmp_path / "config.yaml"}, + ) + + def test_warmup_disabled_no_check(self, tmp_path): + cfg = self._run_config(enabled=False, salt=False, tmp_path=tmp_path) + assert not any(r.rule == "warmup-salt" for r in cfg._check_results) + + def test_warmup_enabled_unsalted_errors(self, tmp_path): + cfg = self._run_config(enabled=True, salt=False, tmp_path=tmp_path) + assert any( + r.rule == "warmup-salt" and r.severity == Severity.ERROR for r in cfg._check_results + ) + + def test_warmup_enabled_salted_passes(self, tmp_path): + cfg = self._run_config(enabled=True, salt=True, tmp_path=tmp_path) + assert any( + r.rule == "warmup-salt" and r.severity != Severity.ERROR for r in cfg._check_results + ) + + def test_missing_settings_block_defaults_to_no_check(self, tmp_path): + cfg = RunConfig.model_validate( + {"concurrency": 64}, + context={"config_path": tmp_path / "config.yaml"}, + ) + assert not any(r.rule == "warmup-salt" for r in cfg._check_results) + + @pytest.mark.unit class TestAccuracyResultModel: def test_scores_accessible(self): - ar = AccuracyResult({ - "cnn_dailymail::llama3_8b": {"score": {"rouge1": "38.73", "rouge2": "16.10"}} - }) + ar = AccuracyResult( + {"cnn_dailymail::llama3_8b": {"score": {"rouge1": "38.73", "rouge2": "16.10"}}} + ) scores = ar.metric_scores() assert scores["cnn_dailymail::llama3_8b"]["rouge1"] == pytest.approx(38.73) @@ -200,6 +256,5 @@ def test_no_check_results_on_valid_input(self): def test_empty_dict_emits_accuracy_valid_error(self): ar = AccuracyResult({}) assert any( - r.rule == "accuracy-valid" and r.severity == Severity.ERROR - for r in ar._check_results + r.rule == "accuracy-valid" and r.severity == Severity.ERROR for r in ar._check_results ) diff --git a/tests/submission_checker/test_loader.py b/tests/submission_checker/test_loader.py index 3703864..1a1750b 100644 --- a/tests/submission_checker/test_loader.py +++ b/tests/submission_checker/test_loader.py @@ -12,6 +12,7 @@ load_accuracy_result, load_point_config, load_result_summary, + load_run_config, load_system_description, ) @@ -110,6 +111,52 @@ def test_load_point_config_valid_returns_check_results(tmp_path): assert "streaming-config" in rules +# --------------------------------------------------------------------------- +# load_run_config +# --------------------------------------------------------------------------- + + +def test_load_run_config_missing_file(tmp_path): + model, results = load_run_config(tmp_path / "missing.yaml") + assert model is None + assert len(results) == 1 + assert results[0].severity == Severity.ERROR + assert "File not found" in results[0].message + + +def test_load_run_config_invalid_yaml(tmp_path): + p = tmp_path / "config.yaml" + p.write_text("key: [unclosed") + model, results = load_run_config(p) + assert model is None + assert results[0].severity == Severity.ERROR + assert "YAML parse error" in results[0].message + + +def test_load_run_config_minimal_no_warmup_check(tmp_path): + p = tmp_path / "config.yaml" + p.write_text(yaml.dump({"concurrency": 64})) + model, results = load_run_config(p) + assert model is not None + assert not any(r.rule == "warmup-salt" for r in results) + + +def test_load_run_config_warmup_unsalted_errors(tmp_path): + p = tmp_path / "config.yaml" + p.write_text(yaml.dump({"settings": {"warmup": {"enabled": True, "salt": False}}})) + model, results = load_run_config(p) + assert model is not None + assert any(r.rule == "warmup-salt" and r.severity == Severity.ERROR for r in results) + + +def test_load_run_config_warmup_salted_passes(tmp_path): + p = tmp_path / "config.yaml" + p.write_text(yaml.dump({"settings": {"warmup": {"enabled": True, "salt": True}}})) + model, results = load_run_config(p) + assert model is not None + assert not any(r.severity == Severity.ERROR for r in results) + + # --------------------------------------------------------------------------- # load_result_summary # --------------------------------------------------------------------------- @@ -187,7 +234,9 @@ def test_load_json_os_error(tmp_path): def test_load_yaml_os_error(tmp_path): """_load_yaml must surface an OSError (e.g. permission denied) as an error message.""" p = tmp_path / "point_64.yaml" - p.write_text(yaml.dump({"concurrency": 64, "runtime_settings": {"load_pattern": "concurrency"}})) + p.write_text( + yaml.dump({"concurrency": 64, "runtime_settings": {"load_pattern": "concurrency"}}) + ) with patch("pathlib.Path.read_text", side_effect=OSError("permission denied")): model, results = load_point_config(p) assert model is None diff --git a/uv.lock b/uv.lock index d0ab6c9..24c5b7f 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,9 @@ revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version == '3.14.*'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", "python_full_version < '3.11'", ] @@ -385,7 +387,9 @@ version = "0.22.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version == '3.14.*'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } @@ -423,6 +427,12 @@ docs = [ { name = "sphinx-autodoc-typehints", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] +[package.dev-dependencies] +dev = [ + { name = "hypothesis" }, + { name = "mutmut" }, +] + [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.1" }, @@ -442,6 +452,12 @@ requires-dist = [ ] provides-extras = ["dev", "docs"] +[package.metadata.requires-dev] +dev = [ + { name = "hypothesis", specifier = ">=6.155.3" }, + { name = "mutmut", specifier = ">=3.6.0" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -509,6 +525,19 @@ 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.155.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/77/13ec9b6390bce44f5badab39837dd6789bbfe6342a2ac611a71537a7756f/hypothesis-6.155.3.tar.gz", hash = "sha256:1e34b17ae9873515384312cb7640abd773eb096c7eef8c0d9c614fa2c306e9bb", size = 477961, upload-time = "2026-06-16T00:33:23.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/23/ce3a543935a01e478349e82f6c1440776f92d4cb346662c4d81574878fed/hypothesis-6.155.3-py3-none-any.whl", hash = "sha256:ede5a3d142d9c5c9f70cb3075541905b228d6c3a682bcec3d4fe0722e9eda127", size = 544401, upload-time = "2026-06-16T00:33:20.497Z" }, +] + [[package]] name = "idna" version = "3.15" @@ -548,6 +577,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "libcst" +version = "1.8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml", marker = "python_full_version != '3.13.*'" }, + { name = "pyyaml-ft", marker = "python_full_version == '3.13.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/cd/337df968b38d94c5aabd3e1b10630f047a2b345f6e1d4456bd9fe7417537/libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b", size = 891354, upload-time = "2025-11-03T22:33:30.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/52/97d5454dee9d014821fe0c88f3dc0e83131b97dd074a4d49537056a75475/libcst-1.8.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a20c5182af04332cc94d8520792befda06d73daf2865e6dddc5161c72ea92cb9", size = 2211698, upload-time = "2025-11-03T22:31:50.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a4/d1205985d378164687af3247a9c8f8bdb96278b0686ac98ab951bc6d336a/libcst-1.8.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36473e47cb199b7e6531d653ee6ffed057de1d179301e6c67f651f3af0b499d6", size = 2093104, upload-time = "2025-11-03T22:31:52.189Z" }, + { url = "https://files.pythonhosted.org/packages/9e/de/1338da681b7625b51e584922576d54f1b8db8fc7ff4dc79121afc5d4d2cd/libcst-1.8.6-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:06fc56335a45d61b7c1b856bfab4587b84cfe31e9d6368f60bb3c9129d900f58", size = 2237419, upload-time = "2025-11-03T22:31:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/50/06/ee66f2d83b870534756e593d464d8b33b0914c224dff3a407e0f74dc04e0/libcst-1.8.6-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6b23d14a7fc0addd9795795763af26b185deb7c456b1e7cc4d5228e69dab5ce8", size = 2300820, upload-time = "2025-11-03T22:31:55.995Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ca/959088729de8e0eac8dd516e4fb8623d8d92bad539060fa85c9e94d418a5/libcst-1.8.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:16cfe0cfca5fd840e1fb2c30afb628b023d3085b30c3484a79b61eae9d6fe7ba", size = 2301201, upload-time = "2025-11-03T22:31:57.347Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4c/2a21a8c452436097dfe1da277f738c3517f3f728713f16d84b9a3d67ca8d/libcst-1.8.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:455f49a93aea4070132c30ebb6c07c2dea0ba6c1fde5ffde59fc45dbb9cfbe4b", size = 2408213, upload-time = "2025-11-03T22:31:59.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/26/8f7b671fad38a515bb20b038718fd2221ab658299119ac9bcec56c2ced27/libcst-1.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:72cca15800ffc00ba25788e4626189fe0bc5fe2a0c1cb4294bce2e4df21cc073", size = 2119189, upload-time = "2025-11-03T22:32:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/5b/bf/ffb23a48e27001165cc5c81c5d9b3d6583b21b7f5449109e03a0020b060c/libcst-1.8.6-cp310-cp310-win_arm64.whl", hash = "sha256:6cad63e3a26556b020b634d25a8703b605c0e0b491426b3e6b9e12ed20f09100", size = 2001736, upload-time = "2025-11-03T22:32:02.986Z" }, + { url = "https://files.pythonhosted.org/packages/dc/15/95c2ecadc0fb4af8a7057ac2012a4c0ad5921b9ef1ace6c20006b56d3b5f/libcst-1.8.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3649a813660fbffd7bc24d3f810b1f75ac98bd40d9d6f56d1f0ee38579021073", size = 2211289, upload-time = "2025-11-03T22:32:04.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/c3/7e1107acd5ed15cf60cc07c7bb64498a33042dc4821874aea3ec4942f3cd/libcst-1.8.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cbe17067055829607c5ba4afa46bfa4d0dd554c0b5a583546e690b7367a29b6", size = 2092927, upload-time = "2025-11-03T22:32:06.209Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ff/0d2be87f67e2841a4a37d35505e74b65991d30693295c46fc0380ace0454/libcst-1.8.6-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:59a7e388c57d21d63722018978a8ddba7b176e3a99bd34b9b84a576ed53f2978", size = 2237002, upload-time = "2025-11-03T22:32:07.559Z" }, + { url = "https://files.pythonhosted.org/packages/69/99/8c4a1b35c7894ccd7d33eae01ac8967122f43da41325223181ca7e4738fe/libcst-1.8.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b6c1248cc62952a3a005792b10cdef2a4e130847be9c74f33a7d617486f7e532", size = 2301048, upload-time = "2025-11-03T22:32:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8b/d1aa811eacf936cccfb386ae0585aa530ea1221ccf528d67144e041f5915/libcst-1.8.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6421a930b028c5ef4a943b32a5a78b7f1bf15138214525a2088f11acbb7d3d64", size = 2300675, upload-time = "2025-11-03T22:32:10.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6b/7b65cd41f25a10c1fef2389ddc5c2b2cc23dc4d648083fa3e1aa7e0eeac2/libcst-1.8.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6d8b67874f2188399a71a71731e1ba2d1a2c3173b7565d1cc7ffb32e8fbaba5b", size = 2407934, upload-time = "2025-11-03T22:32:11.856Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8b/401cfff374bb3b785adfad78f05225225767ee190997176b2a9da9ed9460/libcst-1.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:b0d8c364c44ae343937f474b2e492c1040df96d94530377c2f9263fb77096e4f", size = 2119247, upload-time = "2025-11-03T22:32:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/085f59eaa044b6ff6bc42148a5449df2b7f0ba567307de7782fe85c39ee2/libcst-1.8.6-cp311-cp311-win_arm64.whl", hash = "sha256:5dcaaebc835dfe5755bc85f9b186fb7e2895dda78e805e577fef1011d51d5a5c", size = 2001774, upload-time = "2025-11-03T22:32:14.647Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3c/93365c17da3d42b055a8edb0e1e99f1c60c776471db6c9b7f1ddf6a44b28/libcst-1.8.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c13d5bd3d8414a129e9dccaf0e5785108a4441e9b266e1e5e9d1f82d1b943c9", size = 2206166, upload-time = "2025-11-03T22:32:16.012Z" }, + { url = "https://files.pythonhosted.org/packages/1d/cb/7530940e6ac50c6dd6022349721074e19309eb6aa296e942ede2213c1a19/libcst-1.8.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1472eeafd67cdb22544e59cf3bfc25d23dc94058a68cf41f6654ff4fcb92e09", size = 2083726, upload-time = "2025-11-03T22:32:17.312Z" }, + { url = "https://files.pythonhosted.org/packages/1b/cf/7e5eaa8c8f2c54913160671575351d129170db757bb5e4b7faffed022271/libcst-1.8.6-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:089c58e75cb142ec33738a1a4ea7760a28b40c078ab2fd26b270dac7d2633a4d", size = 2235755, upload-time = "2025-11-03T22:32:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/570ec2b0e9a3de0af9922e3bb1b69a5429beefbc753a7ea770a27ad308bd/libcst-1.8.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c9d7aeafb1b07d25a964b148c0dda9451efb47bbbf67756e16eeae65004b0eb5", size = 2301473, upload-time = "2025-11-03T22:32:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/11/4c/163457d1717cd12181c421a4cca493454bcabd143fc7e53313bc6a4ad82a/libcst-1.8.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:207481197afd328aa91d02670c15b48d0256e676ce1ad4bafb6dc2b593cc58f1", size = 2298899, upload-time = "2025-11-03T22:32:21.765Z" }, + { url = "https://files.pythonhosted.org/packages/35/1d/317ddef3669883619ef3d3395ea583305f353ef4ad87d7a5ac1c39be38e3/libcst-1.8.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:375965f34cc6f09f5f809244d3ff9bd4f6cb6699f571121cebce53622e7e0b86", size = 2408239, upload-time = "2025-11-03T22:32:23.275Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a1/f47d8cccf74e212dd6044b9d6dbc223636508da99acff1d54786653196bc/libcst-1.8.6-cp312-cp312-win_amd64.whl", hash = "sha256:da95b38693b989eaa8d32e452e8261cfa77fe5babfef1d8d2ac25af8c4aa7e6d", size = 2119660, upload-time = "2025-11-03T22:32:24.822Z" }, + { url = "https://files.pythonhosted.org/packages/19/d0/dd313bf6a7942cdf951828f07ecc1a7695263f385065edc75ef3016a3cb5/libcst-1.8.6-cp312-cp312-win_arm64.whl", hash = "sha256:bff00e1c766658adbd09a175267f8b2f7616e5ee70ce45db3d7c4ce6d9f6bec7", size = 1999824, upload-time = "2025-11-03T22:32:26.131Z" }, + { url = "https://files.pythonhosted.org/packages/90/01/723cd467ec267e712480c772aacc5aa73f82370c9665162fd12c41b0065b/libcst-1.8.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7445479ebe7d1aff0ee094ab5a1c7718e1ad78d33e3241e1a1ec65dcdbc22ffb", size = 2206386, upload-time = "2025-11-03T22:32:27.422Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/b944944f910f24c094f9b083f76f61e3985af5a376f5342a21e01e2d1a81/libcst-1.8.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fc3fef8a2c983e7abf5d633e1884c5dd6fa0dcb8f6e32035abd3d3803a3a196", size = 2083945, upload-time = "2025-11-03T22:32:28.847Z" }, + { url = "https://files.pythonhosted.org/packages/36/a1/bd1b2b2b7f153d82301cdaddba787f4a9fc781816df6bdb295ca5f88b7cf/libcst-1.8.6-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1a3a5e4ee870907aa85a4076c914ae69066715a2741b821d9bf16f9579de1105", size = 2235818, upload-time = "2025-11-03T22:32:30.504Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ab/f5433988acc3b4d188c4bb154e57837df9488cc9ab551267cdeabd3bb5e7/libcst-1.8.6-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6609291c41f7ad0bac570bfca5af8fea1f4a27987d30a1fa8b67fe5e67e6c78d", size = 2301289, upload-time = "2025-11-03T22:32:31.812Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/89f4ba7a6f1ac274eec9903a9e9174890d2198266eee8c00bc27eb45ecf7/libcst-1.8.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25eaeae6567091443b5374b4c7d33a33636a2d58f5eda02135e96fc6c8807786", size = 2299230, upload-time = "2025-11-03T22:32:33.242Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/0aa693bc24cce163a942df49d36bf47a7ed614a0cd5598eee2623bc31913/libcst-1.8.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04030ea4d39d69a65873b1d4d877def1c3951a7ada1824242539e399b8763d30", size = 2408519, upload-time = "2025-11-03T22:32:34.678Z" }, + { url = "https://files.pythonhosted.org/packages/db/18/6dd055b5f15afa640fb3304b2ee9df8b7f72e79513814dbd0a78638f4a0e/libcst-1.8.6-cp313-cp313-win_amd64.whl", hash = "sha256:8066f1b70f21a2961e96bedf48649f27dfd5ea68be5cd1bed3742b047f14acde", size = 2119853, upload-time = "2025-11-03T22:32:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ed/5ddb2a22f0b0abdd6dcffa40621ada1feaf252a15e5b2733a0a85dfd0429/libcst-1.8.6-cp313-cp313-win_arm64.whl", hash = "sha256:c188d06b583900e662cd791a3f962a8c96d3dfc9b36ea315be39e0a4c4792ebf", size = 1999808, upload-time = "2025-11-03T22:32:38.1Z" }, + { url = "https://files.pythonhosted.org/packages/25/d3/72b2de2c40b97e1ef4a1a1db4e5e52163fc7e7740ffef3846d30bc0096b5/libcst-1.8.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c41c76e034a1094afed7057023b1d8967f968782433f7299cd170eaa01ec033e", size = 2190553, upload-time = "2025-11-03T22:32:39.819Z" }, + { url = "https://files.pythonhosted.org/packages/0d/20/983b7b210ccc3ad94a82db54230e92599c4a11b9cfc7ce3bc97c1d2df75c/libcst-1.8.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5432e785322aba3170352f6e72b32bea58d28abd141ac37cc9b0bf6b7c778f58", size = 2074717, upload-time = "2025-11-03T22:32:41.373Z" }, + { url = "https://files.pythonhosted.org/packages/13/f2/9e01678fedc772e09672ed99930de7355757035780d65d59266fcee212b8/libcst-1.8.6-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:85b7025795b796dea5284d290ff69de5089fc8e989b25d6f6f15b6800be7167f", size = 2225834, upload-time = "2025-11-03T22:32:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/4a/0d/7bed847b5c8c365e9f1953da274edc87577042bee5a5af21fba63276e756/libcst-1.8.6-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:536567441182a62fb706e7aa954aca034827b19746832205953b2c725d254a93", size = 2287107, upload-time = "2025-11-03T22:32:44.549Z" }, + { url = "https://files.pythonhosted.org/packages/02/f0/7e51fa84ade26c518bfbe7e2e4758b56d86a114c72d60309ac0d350426c4/libcst-1.8.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f04d3672bde1704f383a19e8f8331521abdbc1ed13abb349325a02ac56e5012", size = 2288672, upload-time = "2025-11-03T22:32:45.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cd/15762659a3f5799d36aab1bc2b7e732672722e249d7800e3c5f943b41250/libcst-1.8.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f04febcd70e1e67917be7de513c8d4749d2e09206798558d7fe632134426ea4", size = 2392661, upload-time = "2025-11-03T22:32:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6b/b7f9246c323910fcbe021241500f82e357521495dcfe419004dbb272c7cb/libcst-1.8.6-cp313-cp313t-win_amd64.whl", hash = "sha256:1dc3b897c8b0f7323412da3f4ad12b16b909150efc42238e19cbf19b561cc330", size = 2105068, upload-time = "2025-11-03T22:32:49.145Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0b/4fd40607bc4807ec2b93b054594373d7fa3d31bb983789901afcb9bcebe9/libcst-1.8.6-cp313-cp313t-win_arm64.whl", hash = "sha256:44f38139fa95e488db0f8976f9c7ca39a64d6bc09f2eceef260aa1f6da6a2e42", size = 1985181, upload-time = "2025-11-03T22:32:50.597Z" }, + { url = "https://files.pythonhosted.org/packages/3a/60/4105441989e321f7ad0fd28ffccb83eb6aac0b7cfb0366dab855dcccfbe5/libcst-1.8.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c", size = 2204202, upload-time = "2025-11-03T22:32:52.311Z" }, + { url = "https://files.pythonhosted.org/packages/67/2f/51a6f285c3a183e50cfe5269d4a533c21625aac2c8de5cdf2d41f079320d/libcst-1.8.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661", size = 2083581, upload-time = "2025-11-03T22:32:54.269Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/921b1c19b638860af76cdb28bc81d430056592910b9478eea49e31a7f47a/libcst-1.8.6-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474", size = 2236495, upload-time = "2025-11-03T22:32:55.723Z" }, + { url = "https://files.pythonhosted.org/packages/12/a8/b00592f9bede618cbb3df6ffe802fc65f1d1c03d48a10d353b108057d09c/libcst-1.8.6-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8", size = 2301466, upload-time = "2025-11-03T22:32:57.337Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/790d9002f31580fefd0aec2f373a0f5da99070e04c5e8b1c995d0104f303/libcst-1.8.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a", size = 2300264, upload-time = "2025-11-03T22:32:58.852Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/dc3f10e65bab461be5de57850d2910a02c24c3ddb0da28f0e6e4133c3487/libcst-1.8.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47", size = 2408572, upload-time = "2025-11-03T22:33:00.552Z" }, + { url = "https://files.pythonhosted.org/packages/20/3b/35645157a7590891038b077db170d6dd04335cd2e82a63bdaa78c3297dfe/libcst-1.8.6-cp314-cp314-win_amd64.whl", hash = "sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4", size = 2193917, upload-time = "2025-11-03T22:33:02.354Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a2/1034a9ba7d3e82f2c2afaad84ba5180f601aed676d92b76325797ad60951/libcst-1.8.6-cp314-cp314-win_arm64.whl", hash = "sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9", size = 2078748, upload-time = "2025-11-03T22:33:03.707Z" }, + { url = "https://files.pythonhosted.org/packages/95/a1/30bc61e8719f721a5562f77695e6154e9092d1bdf467aa35d0806dcd6cea/libcst-1.8.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1", size = 2188980, upload-time = "2025-11-03T22:33:05.152Z" }, + { url = "https://files.pythonhosted.org/packages/2c/14/c660204532407c5628e3b615015a902ed2d0b884b77714a6bdbe73350910/libcst-1.8.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4", size = 2074828, upload-time = "2025-11-03T22:33:06.864Z" }, + { url = "https://files.pythonhosted.org/packages/82/e2/c497c354943dff644749f177ee9737b09ed811b8fc842b05709a40fe0d1b/libcst-1.8.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28", size = 2225568, upload-time = "2025-11-03T22:33:08.354Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/45999676d07bd6d0eefa28109b4f97124db114e92f9e108de42ba46a8028/libcst-1.8.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa", size = 2286523, upload-time = "2025-11-03T22:33:10.206Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6c/517d8bf57d9f811862f4125358caaf8cd3320a01291b3af08f7b50719db4/libcst-1.8.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1", size = 2288044, upload-time = "2025-11-03T22:33:11.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/24d7d49478ffb61207f229239879845da40a374965874f5ee60f96b02ddb/libcst-1.8.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996", size = 2392605, upload-time = "2025-11-03T22:33:12.962Z" }, + { url = "https://files.pythonhosted.org/packages/39/c3/829092ead738b71e96a4e96896c96f276976e5a8a58b4473ed813d7c962b/libcst-1.8.6-cp314-cp314t-win_amd64.whl", hash = "sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82", size = 2181581, upload-time = "2025-11-03T22:33:14.514Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/5d6a790a02eb0d9d36c4aed4f41b277497e6178900b2fa29c35353aa45ed/libcst-1.8.6-cp314-cp314t-win_arm64.whl", hash = "sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f", size = 2065000, upload-time = "2025-11-03T22:33:16.257Z" }, +] + [[package]] name = "librt" version = "0.11.0" @@ -633,6 +730,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, ] +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -645,6 +754,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -730,6 +844,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -739,6 +865,24 @@ 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 = "mutmut" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "coverage" }, + { name = "libcst" }, + { name = "pytest" }, + { name = "setproctitle" }, + { name = "textual" }, + { name = "toml", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/b0/ebcae42b90b07756b7aa10c4176835f436332e6c1cb28bc35bae83462382/mutmut-3.6.0.tar.gz", hash = "sha256:bcbd3e4d0d2d4edf3dfb42955417279a8866a3dbbcb87d619f2f3fd0ac7fafda", size = 51538, upload-time = "2026-06-06T07:44:51.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/5a/a0caa3f9db407b5d12c311bd4c87aa67fdd6e3f329377149e303108a1c51/mutmut-3.6.0-py3-none-any.whl", hash = "sha256:a9f5b8dcf6cbf9496769d7cf8bdbba37a0ec709ad98f88d103238b62f10bdf37", size = 47770, upload-time = "2026-06-06T07:44:50.038Z" }, +] + [[package]] name = "mypy" version = "2.1.0" @@ -825,6 +969,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -1082,6 +1235,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "pyyaml-ft" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/eb/5a0d575de784f9a1f94e2b1288c6886f13f34185e13117ed530f32b6f8a8/pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab", size = 141057, upload-time = "2025-06-10T15:32:15.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/ba/a067369fe61a2e57fb38732562927d5bae088c73cb9bb5438736a9555b29/pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6", size = 187027, upload-time = "2025-06-10T15:31:48.722Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c5/a3d2020ce5ccfc6aede0d45bcb870298652ac0cf199f67714d250e0cdf39/pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69", size = 176146, upload-time = "2025-06-10T15:31:50.584Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bb/23a9739291086ca0d3189eac7cd92b4d00e9fdc77d722ab610c35f9a82ba/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0", size = 746792, upload-time = "2025-06-10T15:31:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c2/e8825f4ff725b7e560d62a3609e31d735318068e1079539ebfde397ea03e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42", size = 786772, upload-time = "2025-06-10T15:31:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/35/be/58a4dcae8854f2fdca9b28d9495298fd5571a50d8430b1c3033ec95d2d0e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b", size = 778723, upload-time = "2025-06-10T15:31:56.093Z" }, + { url = "https://files.pythonhosted.org/packages/86/ed/fed0da92b5d5d7340a082e3802d84c6dc9d5fa142954404c41a544c1cb92/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254", size = 758478, upload-time = "2025-06-10T15:31:58.314Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/ac02afe286275980ecb2dcdc0156617389b7e0c0a3fcdedf155c67be2b80/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8", size = 799159, upload-time = "2025-06-10T15:31:59.675Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ac/c492a9da2e39abdff4c3094ec54acac9747743f36428281fb186a03fab76/pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96", size = 158779, upload-time = "2025-06-10T15:32:01.029Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9b/41998df3298960d7c67653669f37710fa2d568a5fc933ea24a6df60acaf6/pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb", size = 191331, upload-time = "2025-06-10T15:32:02.602Z" }, + { url = "https://files.pythonhosted.org/packages/0f/16/2710c252ee04cbd74d9562ebba709e5a284faeb8ada88fcda548c9191b47/pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1", size = 182879, upload-time = "2025-06-10T15:32:04.466Z" }, + { url = "https://files.pythonhosted.org/packages/9a/40/ae8163519d937fa7bfa457b6f78439cc6831a7c2b170e4f612f7eda71815/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49", size = 811277, upload-time = "2025-06-10T15:32:06.214Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/28d82dbff7f87b96f0eeac79b7d972a96b4980c1e445eb6a857ba91eda00/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b", size = 831650, upload-time = "2025-06-10T15:32:08.076Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/161c4566facac7d75a9e182295c223060373d4116dead9cc53a265de60b9/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a", size = 815755, upload-time = "2025-06-10T15:32:09.435Z" }, + { url = "https://files.pythonhosted.org/packages/05/10/f42c48fa5153204f42eaa945e8d1fd7c10d6296841dcb2447bf7da1be5c4/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e", size = 810403, upload-time = "2025-06-10T15:32:11.051Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d2/e369064aa51009eb9245399fd8ad2c562bd0bcd392a00be44b2a824ded7c/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255", size = 835581, upload-time = "2025-06-10T15:32:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/c0/28/26534bed77109632a956977f60d8519049f545abc39215d086e33a61f1f2/pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793", size = 171579, upload-time = "2025-06-10T15:32:14.34Z" }, +] + [[package]] name = "requests" version = "2.34.0" @@ -1144,6 +1321,90 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, ] +[[package]] +name = "setproctitle" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/48/fb401ec8c4953d519d05c87feca816ad668b8258448ff60579ac7a1c1386/setproctitle-1.3.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf555b6299f10a6eb44e4f96d2f5a3884c70ce25dc5c8796aaa2f7b40e72cb1b", size = 18079, upload-time = "2025-09-05T12:49:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a3/c2b0333c2716fb3b4c9a973dd113366ac51b4f8d56b500f4f8f704b4817a/setproctitle-1.3.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:690b4776f9c15aaf1023bb07d7c5b797681a17af98a4a69e76a1d504e41108b7", size = 13099, upload-time = "2025-09-05T12:49:09.222Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f8/17bda581c517678260e6541b600eeb67745f53596dc077174141ba2f6702/setproctitle-1.3.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00afa6fc507967d8c9d592a887cdc6c1f5742ceac6a4354d111ca0214847732c", size = 31793, upload-time = "2025-09-05T12:49:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/27/d1/76a33ae80d4e788ecab9eb9b53db03e81cfc95367ec7e3fbf4989962fedd/setproctitle-1.3.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e02667f6b9fc1238ba753c0f4b0a37ae184ce8f3bbbc38e115d99646b3f4cd3", size = 32779, upload-time = "2025-09-05T12:49:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/59/27/1a07c38121967061564f5e0884414a5ab11a783260450172d4fc68c15621/setproctitle-1.3.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83fcd271567d133eb9532d3b067c8a75be175b2b3b271e2812921a05303a693f", size = 34578, upload-time = "2025-09-05T12:49:13.393Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d4/725e6353935962d8bb12cbf7e7abba1d0d738c7f6935f90239d8e1ccf913/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13fe37951dda1a45c35d77d06e3da5d90e4f875c4918a7312b3b4556cfa7ff64", size = 32030, upload-time = "2025-09-05T12:49:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/e4677ae8e1cb0d549ab558b12db10c175a889be0974c589c428fece5433e/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a05509cfb2059e5d2ddff701d38e474169e9ce2a298cf1b6fd5f3a213a553fe5", size = 33363, upload-time = "2025-09-05T12:49:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/55/d4/69ce66e4373a48fdbb37489f3ded476bb393e27f514968c3a69a67343ae0/setproctitle-1.3.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6da835e76ae18574859224a75db6e15c4c2aaa66d300a57efeaa4c97ca4c7381", size = 31508, upload-time = "2025-09-05T12:49:18.032Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5a/42c1ed0e9665d068146a68326529b5686a1881c8b9197c2664db4baf6aeb/setproctitle-1.3.7-cp310-cp310-win32.whl", hash = "sha256:9e803d1b1e20240a93bac0bc1025363f7f80cb7eab67dfe21efc0686cc59ad7c", size = 12558, upload-time = "2025-09-05T12:49:19.742Z" }, + { url = "https://files.pythonhosted.org/packages/dc/fe/dd206cc19a25561921456f6cb12b405635319299b6f366e0bebe872abc18/setproctitle-1.3.7-cp310-cp310-win_amd64.whl", hash = "sha256:a97200acc6b64ec4cada52c2ecaf1fba1ef9429ce9c542f8a7db5bcaa9dcbd95", size = 13245, upload-time = "2025-09-05T12:49:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/04/cd/1b7ba5cad635510720ce19d7122154df96a2387d2a74217be552887c93e5/setproctitle-1.3.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a600eeb4145fb0ee6c287cb82a2884bd4ec5bbb076921e287039dcc7b7cc6dd0", size = 18085, upload-time = "2025-09-05T12:49:22.183Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1a/b2da0a620490aae355f9d72072ac13e901a9fec809a6a24fc6493a8f3c35/setproctitle-1.3.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97a090fed480471bb175689859532709e28c085087e344bca45cf318034f70c4", size = 13097, upload-time = "2025-09-05T12:49:23.322Z" }, + { url = "https://files.pythonhosted.org/packages/18/2e/bd03ff02432a181c1787f6fc2a678f53b7dacdd5ded69c318fe1619556e8/setproctitle-1.3.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1607b963e7b53e24ec8a2cb4e0ab3ae591d7c6bf0a160feef0551da63452b37f", size = 32191, upload-time = "2025-09-05T12:49:24.567Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/1e62fc0937a8549f2220445ed2175daacee9b6764c7963b16148119b016d/setproctitle-1.3.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a20fb1a3974e2dab857870cf874b325b8705605cb7e7e8bcbb915bca896f52a9", size = 33203, upload-time = "2025-09-05T12:49:25.871Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/65edc65db3fa3df400cf13b05e9d41a3c77517b4839ce873aa6b4043184f/setproctitle-1.3.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8d961bba676e07d77665204f36cffaa260f526e7b32d07ab3df6a2c1dfb44ba", size = 34963, upload-time = "2025-09-05T12:49:27.044Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/89157e3de997973e306e44152522385f428e16f92f3cf113461489e1e2ee/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:db0fd964fbd3a9f8999b502f65bd2e20883fdb5b1fae3a424e66db9a793ed307", size = 32398, upload-time = "2025-09-05T12:49:28.909Z" }, + { url = "https://files.pythonhosted.org/packages/4a/18/77a765a339ddf046844cb4513353d8e9dcd8183da9cdba6e078713e6b0b2/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:db116850fcf7cca19492030f8d3b4b6e231278e8fe097a043957d22ce1bdf3ee", size = 33657, upload-time = "2025-09-05T12:49:30.323Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/f0b6205c64d74d2a24a58644a38ec77bdbaa6afc13747e75973bf8904932/setproctitle-1.3.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:316664d8b24a5c91ee244460bdaf7a74a707adaa9e14fbe0dc0a53168bb9aba1", size = 31836, upload-time = "2025-09-05T12:49:32.309Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/e1277f9ba302f1a250bbd3eedbbee747a244b3cc682eb58fb9733968f6d8/setproctitle-1.3.7-cp311-cp311-win32.whl", hash = "sha256:b74774ca471c86c09b9d5037c8451fff06bb82cd320d26ae5a01c758088c0d5d", size = 12556, upload-time = "2025-09-05T12:49:33.529Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/822a23f17e9003dfdee92cd72758441ca2a3680388da813a371b716fb07f/setproctitle-1.3.7-cp311-cp311-win_amd64.whl", hash = "sha256:acb9097213a8dd3410ed9f0dc147840e45ca9797785272928d4be3f0e69e3be4", size = 13243, upload-time = "2025-09-05T12:49:34.553Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f0/2dc88e842077719d7384d86cc47403e5102810492b33680e7dadcee64cd8/setproctitle-1.3.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dc99aec591ab6126e636b11035a70991bc1ab7a261da428491a40b84376654e", size = 18049, upload-time = "2025-09-05T12:49:36.241Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b4/50940504466689cda65680c9e9a1e518e5750c10490639fa687489ac7013/setproctitle-1.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdd8aa571b7aa39840fdbea620e308a19691ff595c3a10231e9ee830339dd798", size = 13079, upload-time = "2025-09-05T12:49:38.088Z" }, + { url = "https://files.pythonhosted.org/packages/d0/99/71630546b9395b095f4082be41165d1078204d1696c2d9baade3de3202d0/setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629", size = 32932, upload-time = "2025-09-05T12:49:39.271Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/cee06af4ffcfb0e8aba047bd44f5262e644199ae7527ae2c1f672b86495c/setproctitle-1.3.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6915964a6dda07920a1159321dcd6d94fc7fc526f815ca08a8063aeca3c204f1", size = 33736, upload-time = "2025-09-05T12:49:40.565Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/a5949a8bb06ef5e7df214fc393bb2fb6aedf0479b17214e57750dfdd0f24/setproctitle-1.3.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cff72899861c765bd4021d1ff1c68d60edc129711a2fdba77f9cb69ef726a8b6", size = 35605, upload-time = "2025-09-05T12:49:42.362Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3a/50caca532a9343828e3bf5778c7a84d6c737a249b1796d50dd680290594d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7cb05bd446687ff816a3aaaf831047fc4c364feff7ada94a66024f1367b448c", size = 33143, upload-time = "2025-09-05T12:49:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ca/14/b843a251296ce55e2e17c017d6b9f11ce0d3d070e9265de4ecad948b913d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3a57b9a00de8cae7e2a1f7b9f0c2ac7b69372159e16a7708aa2f38f9e5cc987a", size = 34434, upload-time = "2025-09-05T12:49:45.31Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b7/06145c238c0a6d2c4bc881f8be230bb9f36d2bf51aff7bddcb796d5eed67/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739", size = 32795, upload-time = "2025-09-05T12:49:46.419Z" }, + { url = "https://files.pythonhosted.org/packages/ef/dc/ef76a81fac9bf27b84ed23df19c1f67391a753eed6e3c2254ebcb5133f56/setproctitle-1.3.7-cp312-cp312-win32.whl", hash = "sha256:b0304f905efc845829ac2bc791ddebb976db2885f6171f4a3de678d7ee3f7c9f", size = 12552, upload-time = "2025-09-05T12:49:47.635Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5b/a9fe517912cd6e28cf43a212b80cb679ff179a91b623138a99796d7d18a0/setproctitle-1.3.7-cp312-cp312-win_amd64.whl", hash = "sha256:9888ceb4faea3116cf02a920ff00bfbc8cc899743e4b4ac914b03625bdc3c300", size = 13247, upload-time = "2025-09-05T12:49:49.16Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2f/fcedcade3b307a391b6e17c774c6261a7166aed641aee00ed2aad96c63ce/setproctitle-1.3.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3736b2a423146b5e62230502e47e08e68282ff3b69bcfe08a322bee73407922", size = 18047, upload-time = "2025-09-05T12:49:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/23/ae/afc141ca9631350d0a80b8f287aac79a76f26b6af28fd8bf92dae70dc2c5/setproctitle-1.3.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3384e682b158d569e85a51cfbde2afd1ab57ecf93ea6651fe198d0ba451196ee", size = 13073, upload-time = "2025-09-05T12:49:51.46Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/0a4f00315bc02510395b95eec3d4aa77c07192ee79f0baae77ea7b9603d8/setproctitle-1.3.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0564a936ea687cd24dffcea35903e2a20962aa6ac20e61dd3a207652401492dd", size = 33284, upload-time = "2025-09-05T12:49:52.741Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/adf3c4c0a2173cb7920dc9df710bcc67e9bcdbf377e243b7a962dc31a51a/setproctitle-1.3.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5d1cb3f81531f0eb40e13246b679a1bdb58762b170303463cb06ecc296f26d0", size = 34104, upload-time = "2025-09-05T12:49:54.416Z" }, + { url = "https://files.pythonhosted.org/packages/52/4f/6daf66394152756664257180439d37047aa9a1cfaa5e4f5ed35e93d1dc06/setproctitle-1.3.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a7d159e7345f343b44330cbba9194169b8590cb13dae940da47aa36a72aa9929", size = 35982, upload-time = "2025-09-05T12:49:56.295Z" }, + { url = "https://files.pythonhosted.org/packages/1b/62/f2c0595403cf915db031f346b0e3b2c0096050e90e0be658a64f44f4278a/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b5074649797fd07c72ca1f6bff0406f4a42e1194faac03ecaab765ce605866f", size = 33150, upload-time = "2025-09-05T12:49:58.025Z" }, + { url = "https://files.pythonhosted.org/packages/a0/29/10dd41cde849fb2f9b626c846b7ea30c99c81a18a5037a45cc4ba33c19a7/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61e96febced3f61b766115381d97a21a6265a0f29188a791f6df7ed777aef698", size = 34463, upload-time = "2025-09-05T12:49:59.424Z" }, + { url = "https://files.pythonhosted.org/packages/71/3c/cedd8eccfaf15fb73a2c20525b68c9477518917c9437737fa0fda91e378f/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:047138279f9463f06b858e579cc79580fbf7a04554d24e6bddf8fe5dddbe3d4c", size = 32848, upload-time = "2025-09-05T12:50:01.107Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3e/0a0e27d1c9926fecccfd1f91796c244416c70bf6bca448d988638faea81d/setproctitle-1.3.7-cp313-cp313-win32.whl", hash = "sha256:7f47accafac7fe6535ba8ba9efd59df9d84a6214565108d0ebb1199119c9cbbd", size = 12544, upload-time = "2025-09-05T12:50:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/36/1b/6bf4cb7acbbd5c846ede1c3f4d6b4ee52744d402e43546826da065ff2ab7/setproctitle-1.3.7-cp313-cp313-win_amd64.whl", hash = "sha256:fe5ca35aeec6dc50cabab9bf2d12fbc9067eede7ff4fe92b8f5b99d92e21263f", size = 13235, upload-time = "2025-09-05T12:50:16.89Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a4/d588d3497d4714750e3eaf269e9e8985449203d82b16b933c39bd3fc52a1/setproctitle-1.3.7-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:10e92915c4b3086b1586933a36faf4f92f903c5554f3c34102d18c7d3f5378e9", size = 18058, upload-time = "2025-09-05T12:50:02.501Z" }, + { url = "https://files.pythonhosted.org/packages/05/77/7637f7682322a7244e07c373881c7e982567e2cb1dd2f31bd31481e45500/setproctitle-1.3.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:de879e9c2eab637f34b1a14c4da1e030c12658cdc69ee1b3e5be81b380163ce5", size = 13072, upload-time = "2025-09-05T12:50:03.601Z" }, + { url = "https://files.pythonhosted.org/packages/52/09/f366eca0973cfbac1470068d1313fa3fe3de4a594683385204ec7f1c4101/setproctitle-1.3.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c18246d88e227a5b16248687514f95642505000442165f4b7db354d39d0e4c29", size = 34490, upload-time = "2025-09-05T12:50:04.948Z" }, + { url = "https://files.pythonhosted.org/packages/71/36/611fc2ed149fdea17c3677e1d0df30d8186eef9562acc248682b91312706/setproctitle-1.3.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7081f193dab22df2c36f9fc6d113f3793f83c27891af8fe30c64d89d9a37e152", size = 35267, upload-time = "2025-09-05T12:50:06.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/a4/64e77d0671446bd5a5554387b69e1efd915274686844bea733714c828813/setproctitle-1.3.7-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cc9b901ce129350637426a89cfd650066a4adc6899e47822e2478a74023ff7c", size = 37376, upload-time = "2025-09-05T12:50:07.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/ad9c664fe524fb4a4b2d3663661a5c63453ce851736171e454fa2cdec35c/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80e177eff2d1ec172188d0d7fd9694f8e43d3aab76a6f5f929bee7bf7894e98b", size = 33963, upload-time = "2025-09-05T12:50:09.056Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a36de7caf2d90c4c28678da1466b47495cbbad43badb4e982d8db8167ed4/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:23e520776c445478a67ee71b2a3c1ffdafbe1f9f677239e03d7e2cc635954e18", size = 35550, upload-time = "2025-09-05T12:50:10.791Z" }, + { url = "https://files.pythonhosted.org/packages/dd/68/17e8aea0ed5ebc17fbf03ed2562bfab277c280e3625850c38d92a7b5fcd9/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5fa1953126a3b9bd47049d58c51b9dac72e78ed120459bd3aceb1bacee72357c", size = 33727, upload-time = "2025-09-05T12:50:12.032Z" }, + { url = "https://files.pythonhosted.org/packages/b2/33/90a3bf43fe3a2242b4618aa799c672270250b5780667898f30663fd94993/setproctitle-1.3.7-cp313-cp313t-win32.whl", hash = "sha256:4a5e212bf438a4dbeece763f4962ad472c6008ff6702e230b4f16a037e2f6f29", size = 12549, upload-time = "2025-09-05T12:50:13.074Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/50d1f07f3032e1f23d814ad6462bc0a138f369967c72494286b8a5228e40/setproctitle-1.3.7-cp313-cp313t-win_amd64.whl", hash = "sha256:cf2727b733e90b4f874bac53e3092aa0413fe1ea6d4f153f01207e6ce65034d9", size = 13243, upload-time = "2025-09-05T12:50:14.146Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/43ac3a98414f91d1b86a276bc2f799ad0b4b010e08497a95750d5bc42803/setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63", size = 18052, upload-time = "2025-09-05T12:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2c/dc258600a25e1a1f04948073826bebc55e18dbd99dc65a576277a82146fa/setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e", size = 13071, upload-time = "2025-09-05T12:50:19.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/8e3bb082992f19823d831f3d62a89409deb6092e72fc6940962983ffc94f/setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f", size = 33180, upload-time = "2025-09-05T12:50:20.395Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/ae692a20276d1159dd0cf77b0bcf92cbb954b965655eb4a69672099bb214/setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5", size = 34043, upload-time = "2025-09-05T12:50:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/6a092076324dd4dac1a6d38482bedebbff5cf34ef29f58585ec76e47bc9d/setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17", size = 35892, upload-time = "2025-09-05T12:50:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1a/8836b9f28cee32859ac36c3df85aa03e1ff4598d23ea17ca2e96b5845a8f/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e", size = 32898, upload-time = "2025-09-05T12:50:25.617Z" }, + { url = "https://files.pythonhosted.org/packages/ef/22/8fabdc24baf42defb599714799d8445fe3ae987ec425a26ec8e80ea38f8e/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0", size = 34308, upload-time = "2025-09-05T12:50:26.827Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/b9bee9de6c8cdcb3b3a6cb0b3e773afdb86bbbc1665a3bfa424a4294fda2/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8", size = 32536, upload-time = "2025-09-05T12:50:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/37/0c/75e5f2685a5e3eda0b39a8b158d6d8895d6daf3ba86dec9e3ba021510272/setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed", size = 12731, upload-time = "2025-09-05T12:50:43.955Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/acddbce90d1361e1786e1fb421bc25baeb0c22ef244ee5d0176511769ec8/setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416", size = 13464, upload-time = "2025-09-05T12:50:45.057Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/20886c8ff2e6d85e3cabadab6aab9bb90acaf1a5cfcb04d633f8d61b2626/setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3", size = 18062, upload-time = "2025-09-05T12:50:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/9a/60/26dfc5f198715f1343b95c2f7a1c16ae9ffa45bd89ffd45a60ed258d24ea/setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309", size = 13075, upload-time = "2025-09-05T12:50:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/980b01f50d51345dd513047e3ba9e96468134b9181319093e61db1c47188/setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b", size = 34744, upload-time = "2025-09-05T12:50:32.777Z" }, + { url = "https://files.pythonhosted.org/packages/86/b4/82cd0c86e6d1c4538e1a7eb908c7517721513b801dff4ba3f98ef816a240/setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45", size = 35589, upload-time = "2025-09-05T12:50:34.13Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4f/9f6b2a7417fd45673037554021c888b31247f7594ff4bd2239918c5cd6d0/setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4", size = 37698, upload-time = "2025-09-05T12:50:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/20/92/927b7d4744aac214d149c892cb5fa6dc6f49cfa040cb2b0a844acd63dcaf/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1", size = 34201, upload-time = "2025-09-05T12:50:36.697Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0c/fd4901db5ba4b9d9013e62f61d9c18d52290497f956745cd3e91b0d80f90/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070", size = 35801, upload-time = "2025-09-05T12:50:38.314Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/54b496ac724e60e61cc3447f02690105901ca6d90da0377dffe49ff99fc7/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73", size = 33958, upload-time = "2025-09-05T12:50:39.841Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/c84bb045ebf8c6fdc7f7532319e86f8380d14bbd3084e6348df56bdfe6fd/setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2", size = 12745, upload-time = "2025-09-05T12:50:41.377Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/3a5a4f9952972791a9114ac01dfc123f0df79903577a3e0a7a404a695586/setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a", size = 13469, upload-time = "2025-09-05T12:50:42.67Z" }, + { url = "https://files.pythonhosted.org/packages/34/8a/aff5506ce89bc3168cb492b18ba45573158d528184e8a9759a05a09088a9/setproctitle-1.3.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:eb440c5644a448e6203935ed60466ec8d0df7278cd22dc6cf782d07911bcbea6", size = 12654, upload-time = "2025-09-05T12:51:17.141Z" }, + { url = "https://files.pythonhosted.org/packages/41/89/5b6f2faedd6ced3d3c085a5efbd91380fb1f61f4c12bc42acad37932f4e9/setproctitle-1.3.7-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:502b902a0e4c69031b87870ff4986c290ebbb12d6038a70639f09c331b18efb2", size = 14284, upload-time = "2025-09-05T12:51:18.393Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c0/4312fed3ca393a29589603fd48f17937b4ed0638b923bac75a728382e730/setproctitle-1.3.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f6f268caeabb37ccd824d749e7ce0ec6337c4ed954adba33ec0d90cc46b0ab78", size = 13282, upload-time = "2025-09-05T12:51:19.703Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5b/5e1c117ac84e3cefcf8d7a7f6b2461795a87e20869da065a5c087149060b/setproctitle-1.3.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b1cac6a4b0252b8811d60b6d8d0f157c0fdfed379ac89c25a914e6346cf355a1", size = 12587, upload-time = "2025-09-05T12:51:21.195Z" }, + { url = "https://files.pythonhosted.org/packages/73/02/b9eadc226195dcfa90eed37afe56b5dd6fa2f0e5220ab8b7867b8862b926/setproctitle-1.3.7-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1704c9e041f2b1dc38f5be4552e141e1432fba3dd52c72eeffd5bc2db04dc65", size = 14286, upload-time = "2025-09-05T12:51:22.61Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/1be1d2a53c2a91ec48fa2ff4a409b395f836798adf194d99de9c059419ea/setproctitle-1.3.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b08b61976ffa548bd5349ce54404bf6b2d51bd74d4f1b241ed1b0f25bce09c3a", size = 13282, upload-time = "2025-09-05T12:51:24.094Z" }, +] + [[package]] name = "snowballstemmer" version = "3.0.1" @@ -1153,6 +1414,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] +[[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 = "soupsieve" version = "2.8.3" @@ -1230,7 +1500,9 @@ version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version == '3.14.*'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", ] dependencies = [ { name = "alabaster", marker = "python_full_version >= '3.12'" }, @@ -1292,7 +1564,9 @@ version = "3.10.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version == '3.14.*'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", ] dependencies = [ { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, @@ -1370,6 +1644,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] +[[package]] +name = "textual" +version = "8.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/7a/c519db0aba5024f86e71e9631810bfdd6866ed2c8695bd7fa34b90e7ef59/textual-8.2.7.tar.gz", hash = "sha256:658f568ff81e30ed43890c3e07520390e5cf1b4763822006e060656b0a88f105", size = 1859249, upload-time = "2026-05-19T10:52:49.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/f5/c1e18bc0707300a0e90204343abbf7d7acd6fb7ebe03a6d4893b99a234b8/textual-8.2.7-py3-none-any.whl", hash = "sha256:4caaa13a90bc4cf9c6c862c067ccd34fe84e9c161710a2a907a8026313b6bd73", size = 731129, upload-time = "2026-05-19T10:52:51.773Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + [[package]] name = "tomli" version = "2.4.1" @@ -1454,6 +1754,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + [[package]] name = "urllib3" version = "2.7.0"