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.

## 3.21.0.0 (2026-09-10)

- Using Vale v3.21.0
Expand Down
33 changes: 31 additions & 2 deletions vale/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."""
Expand Down Expand Up @@ -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"
Expand All @@ -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)
Expand Down