diff --git a/brainstat/_utils.py b/brainstat/_utils.py index cee4914f..2ecf865a 100644 --- a/brainstat/_utils.py +++ b/brainstat/_utils.py @@ -150,7 +150,7 @@ def deprecated_func(*args, **kwargs): def _download_file( url: str, output_file: Path, overwrite: bool = False, verbose=True ) -> None: - """Downloads a file. + """Downloads a file with retry logic for network failures. Parameters ---------- @@ -163,14 +163,36 @@ def _download_file( verbose : bool If true, print a download message, defaults to True. """ + import time + from http.client import RemoteDisconnected + from urllib.error import URLError if output_file.exists() and not overwrite: return if verbose: logger.info("Downloading " + str(output_file) + " from " + url + ".") - with urllib.request.urlopen(url) as response, open(output_file, "wb") as out_file: - shutil.copyfileobj(response, out_file) + + # Retry logic for intermittent network failures + max_retries = 3 + retry_delay = 2 # seconds + + for attempt in range(max_retries): + try: + with urllib.request.urlopen(url, timeout=30) as response, open(output_file, "wb") as out_file: + shutil.copyfileobj(response, out_file) + return # Success, exit function + except (RemoteDisconnected, URLError, TimeoutError) as e: + if attempt < max_retries - 1: + logger.warning( + f"Download attempt {attempt + 1}/{max_retries} failed: {e}. " + f"Retrying in {retry_delay} seconds..." + ) + time.sleep(retry_delay) + retry_delay *= 2 # Exponential backoff + else: + logger.error(f"Download failed after {max_retries} attempts.") + raise # Re-raise the exception after all retries fail diff --git a/brainstat/context/histology.py b/brainstat/context/histology.py index b7d13511..095ae6f5 100644 --- a/brainstat/context/histology.py +++ b/brainstat/context/histology.py @@ -157,6 +157,15 @@ def read_histology_profile( with h5py.File(histology_file, "r") as h5_file: if template == "fslr32k": + # Known issue #369: The fslr32k data file on the server actually contains + # fs_LR_64k resolution data (64984 vertices) instead of fs_LR_32k (32492 vertices). + # This is a data hosting issue. Users expecting 32k resolution data should be aware + # that they will receive 64k resolution data until this is fixed on the server. + logger.warning( + "Known issue: The fslr32k histology profile data currently contains " + "fs_LR_64k resolution (64984 vertices) instead of fs_LR_32k (32492 vertices). " + "See https://github.com/MICA-MNI/BrainStat/issues/369 for details." + ) profiles = h5_file.get("fs_LR_64k")[...] else: profiles = h5_file.get(template)[...] @@ -196,6 +205,12 @@ def download_histology_profiles( data_dir.mkdir(parents=True, exist_ok=True) if template == "fslr32k": output_file = data_dir / "histology_fslr32k.h5" + # Known issue #369: warn users about data resolution mismatch + logger.warning( + "Note: The fslr32k histology profile data currently contains " + "fs_LR_64k resolution (64984 vertices) instead of fs_LR_32k. " + "See https://github.com/MICA-MNI/BrainStat/issues/369 for details." + ) else: output_file = data_dir / ("histology_" + template + ".h5") diff --git a/brainstat/tests/test_histology.py b/brainstat/tests/test_histology.py index f29f8404..d44fc173 100644 --- a/brainstat/tests/test_histology.py +++ b/brainstat/tests/test_histology.py @@ -18,9 +18,6 @@ def test_urls(template): template : list Template names. """ - if template == "fslr32k": - pytest.skip("Skipping fslr32k due to known netneurotools 0.3.0 Unicode bug") - try: r = requests.head(json["bigbrain_profiles"][template]["url"], timeout=10) assert r.status_code == 200