From 97976111d0a26f6460161c18d2a9257c15a7221a Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Mon, 14 Sep 2026 12:12:26 +0100 Subject: [PATCH] Retry transient Vale download failures --- CHANGELOG.md | 4 ++++ vale/main.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f2d37e..88059de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Retry transient failures when downloading Vale. + ## 3.21.0.0 (2026-09-10) - Using Vale v3.21.0 diff --git a/vale/main.py b/vale/main.py index 4f74d02..adbcfad 100755 --- a/vale/main.py +++ b/vale/main.py @@ -8,10 +8,12 @@ import sys import tarfile import tempfile +import time import zipfile from functools import partial from pathlib import Path -from typing import Optional, Tuple +from typing import Any, Optional, Tuple +from urllib.error import HTTPError, URLError from urllib.request import urlopen import vale @@ -34,6 +36,9 @@ def major_minor_patch(version: str) -> str: # re-release or upload deleted versions, every uploaded version must be unique. vale_bin_version = major_minor_patch(importlib_metadata.version("vale")) +_DOWNLOAD_ATTEMPTS = 3 +_RETRYABLE_HTTP_STATUS_CODES = {408, 429} + def get_target() -> Tuple[str, str, str]: """Return Vale's target OS, architecture, and extension to download.""" @@ -106,6 +111,30 @@ def extract_vale( return f"{vale_tmp_path}" +def _open_download_url(url: str) -> Any: + """Open a download URL, retrying transient failures.""" + for attempt in range(_DOWNLOAD_ATTEMPTS): + try: + return urlopen(url) + except HTTPError as error: + retryable = error.code in _RETRYABLE_HTTP_STATUS_CODES or 500 <= error.code < 600 + if not retryable or attempt == _DOWNLOAD_ATTEMPTS - 1: + raise + error.close() + except URLError: + if attempt == _DOWNLOAD_ATTEMPTS - 1: + raise + + delay = 2**attempt + print( + f"* Download failed. Retrying in {delay} second(s)...", + file=sys.stderr, + ) + time.sleep(delay) + + raise AssertionError("unreachable") + + def download_vale_if_missing() -> str: """Download vale only if missing.""" vale_bin_path = Path(vale.__file__).parent / "vale_bin" @@ -125,7 +154,7 @@ def download_vale_if_missing() -> str: f"/v{vale_bin_version}/{vale_archive_file_name}" ) - url = urlopen(url_str) + url = _open_download_url(url_str) with tempfile.TemporaryDirectory() as temp_dir: temp_dir_path = Path(temp_dir)