Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Retry transient failures when downloading Vale.
Comment thread
adamtheturtle marked this conversation as resolved.

## 3.25.1.0 (2026-10-23)

- Using Vale v3.25.1
Expand Down
43 changes: 41 additions & 2 deletions vale/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@

import os
import platform
import random
import shutil
import subprocess
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
Expand All @@ -34,6 +37,8 @@ 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"))

_RETRYABLE_HTTP_STATUS_CODES = {408, 429, *range(500, 600)}


def get_target() -> Tuple[str, str, str]:
"""Return Vale's target OS, architecture, and extension to download."""
Expand Down Expand Up @@ -106,6 +111,40 @@ def extract_vale(
return f"{vale_tmp_path}"


def _open_download_url(url: str) -> Any:
"""Open a download URL, retrying transient failures."""
timeout_seconds = 60
max_download_attempts = 5
scale_factor = 2

for attempt in range(max_download_attempts):
try:
return urlopen(url, timeout=timeout_seconds)
except HTTPError as error:
if (
error.code not in _RETRYABLE_HTTP_STATUS_CODES
or attempt == max_download_attempts - 1
):
raise
failure_reason = f"HTTP {error.code}: {error.reason}"
error.close()
except (URLError, TimeoutError) as error:
if attempt == max_download_attempts - 1:
raise
failure_reason = str(error)

jitter_seconds = random.uniform(0, 10)
delay_seconds = (scale_factor * (2**attempt)) + jitter_seconds
print(
f"* Download failed ({failure_reason}). "
f"Retrying in {delay_seconds} second(s)...",
file=sys.stderr,
)
time.sleep(delay_seconds)

raise AssertionError("unreachable")


def download_vale_if_missing() -> str:
"""Download vale only if missing."""
vale_bin_path = Path(vale.__file__).parent / "vale_bin"
Expand All @@ -125,7 +164,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)
Expand Down