Source code for brainglobe_atlasapi.utils

"""Provide utility functions for brainglobe-atlasapi."""

import configparser
import json
import logging
import re
from pathlib import Path
from time import sleep
from typing import Callable, Optional

import requests
import tifffile
from rich.panel import Panel
from rich.pretty import Pretty
from rich.progress import (
    BarColumn,
    DownloadColumn,
    Progress,
    TextColumn,
    TimeRemainingColumn,
    TransferSpeedColumn,
)
from rich.table import Table
from rich.text import Text

from brainglobe_atlasapi import config, descriptors

logging.getLogger("urllib3").setLevel(logging.WARNING)


def _rich_atlas_metadata(atlas_name, metadata):
    orange = "#f59e42"
    dimorange = "#b56510"
    gray = "#A9A9A9"
    mocassin = "#FFE4B5"

    # Create a rich table
    tb = Table(
        box=None,
        show_lines=False,
        title=atlas_name.replace("_", " ").capitalize(),
        title_style=f"bold {orange}",
    )

    # Add entries to table
    tb.add_column(
        style=f"bold {mocassin}",
        justify="right",
        min_width=8,
        max_width=40,
    )
    tb.add_column(min_width=20, max_width=48)

    tb.add_row(
        "name:",
        Text.from_markup(
            metadata["name"] + f" [{gray}](v{metadata['version']})"
        ),
    )
    tb.add_row("species:", Text.from_markup(f"[i]{metadata['species']}"))
    tb.add_row("citation:", Text.from_markup(f"{metadata['citation']}"))
    tb.add_row("link:", Text.from_markup(metadata["atlas_link"]))
    tb.add_row("")
    tb.add_row(
        "orientation:",
        Text.from_markup(f"[bold]{metadata['orientation']}"),
    )
    tb.add_row("symmetric:", Pretty(metadata["symmetric"]))
    tb.add_row("resolution:", Pretty(metadata["resolution"]))
    tb.add_row("shape:", Pretty(metadata["shape"]))

    # Fit into panel and yield
    panel = Panel.fit(tb, border_style=dimorange)
    return panel


[docs] def atlas_repr_from_name(name): """Generate dictionary with atlas description given the name.""" parts = name.split("_") # if atlas name with no version: version_str = ( parts.pop() if not any(parts[-1].endswith(unit) for unit in descriptors.RESOLUTION) else None ) resolution_str = parts.pop() atlas_name = "_".join(parts) # For specified version: if version_str: major_vers, minor_vers = version_str[1:].split(".") else: major_vers, minor_vers = None, None # separate unit from resolution unit = None for res_unit in descriptors.RESOLUTION: if resolution_str.endswith(res_unit): unit = res_unit resolution_str = resolution_str[: -len(res_unit)] break result = dict( name=atlas_name, major_vers=major_vers, minor_vers=minor_vers, resolution=resolution_str, ) if unit: result["unit"] = unit return result
[docs] def atlas_name_from_repr( name, resolution, major_vers=None, minor_vers=None, unit="um" ): """Generate atlas name given a description.""" if major_vers is None and minor_vers is None: return f"{name}_{resolution}{unit}" else: return f"{name}_{resolution}{unit}_v{major_vers}.{minor_vers}"
### Web requests
[docs] def check_internet_connection( url="http://www.google.com/", timeout=5, raise_error=True ): """Check that there is an internet connection url : str url to use for testing (Default value = 'http://www.google.com/') timeout : int timeout to wait for [in seconds] (Default value = 5). raise_error : bool if false, warning but no error. """ urls_to_try = [url] if url == "http://www.google.com/": # fallback URLs that are globally accessible fallback_urls = [ "https://pypi.org/", "https://www.bing.com/", "https://gitee.com/", "http://perdu.com/", ] urls_to_try.extend([u for u in fallback_urls if u != url]) for current_url in urls_to_try: try: _ = requests.get(current_url, timeout=timeout) return True except requests.ConnectionError: continue if not raise_error: print("No internet connection available.") else: raise ConnectionError( "No internet connection, try again when you are " "connected to the internet." ) return False
[docs] def check_gin_status(timeout=5, raise_error=True): """Check that the GIN server is up. timeout : int timeout to wait for [in seconds] (Default value = 5). raise_error : bool if false, warning but no error. """ url = "https://gin.g-node.org/" try: _ = requests.get(url, timeout=timeout) return True except (requests.ConnectionError, requests.exceptions.Timeout) as e: error_message = "GIN server is down." if not raise_error: print(error_message) else: raise ConnectionError(error_message) from e return False
[docs] def retrieve_over_http( url, output_file_path, fn_update: Optional[Callable[[int, int], None]] = None, ): """Download file from remote location, with progress bar. Parameters ---------- url : str Remote URL. output_file_path : str or Path Full file destination for download. fn_update : Callable Handler function to update during download. Takes completed and total bytes. """ # Make Rich progress bar progress = Progress( TextColumn("[bold]Downloading...", justify="right"), BarColumn(bar_width=None), "{task.percentage:>3.1f}%", "•", DownloadColumn(), "• speed:", TransferSpeedColumn(), "• ETA:", TimeRemainingColumn(), ) CHUNK_SIZE = 4096 try: response = requests.get(url, stream=True) with progress: tot = int(response.headers.get("content-length", 0)) if tot == 0: try: tot = get_download_size(url) except Exception: tot = 0 task_id = progress.add_task( "download", filename=output_file_path.name, start=True, total=tot, ) with open(output_file_path, "wb") as fout: completed = 0 for chunk in response.iter_content(chunk_size=CHUNK_SIZE): fout.write(chunk) adv = len(chunk) completed += adv progress.update(task_id, completed=min(completed, tot)) if fn_update: # update handler with completed and total bytes fn_update(completed, tot) except requests.exceptions.ConnectionError: output_file_path.unlink(missing_ok=True) raise requests.exceptions.ConnectionError( f"Could not download file from {url}" )
[docs] def get_download_size(url: str) -> int: """Get file size based on the MB value on the "src" page of each atlas. Parameters ---------- url : str atlas file url (in a repo, make sure the "raw" url is passed) Returns ------- int size of the file to download Raises ------ requests.exceptions.HTTPError: If there's an issue with HTTP request. ValueError: If the file size cannot be extracted from the response. IndexError: If the url is not formatted as expected """ try: # Replace the 'raw' in the url with 'src' url_split = url.split("/") url_split[5] = "src" url = "/".join(url_split) response = requests.get(url) response.raise_for_status() response_string = response.content.decode("utf-8") search_result = re.search( r"([0-9]+\.[0-9] [MGK]B)|([0-9]+ [MGK]B)", response_string ) assert search_result is not None size_string = search_result.group() assert size_string is not None size = float(size_string[:-3]) prefix = size_string[-2] if prefix == "G": size *= 1e9 elif prefix == "M": size *= 1e6 elif prefix == "K": size *= 1e3 return int(size) except requests.exceptions.HTTPError as e: raise e except AssertionError: raise ValueError("File size information not found in the response.") except IndexError: raise IndexError("Improperly formatted URL")
[docs] def conf_from_url(url) -> configparser.ConfigParser: """Read conf file from a URL and cache a copy of the configuration file in the brainglobe directory. Parameters ---------- url : str URL of the configuration file. Ensure it's the raw URL for repository files (e.g., from GIN raw content). Returns ------- configparser.ConfigParser A ConfigParser object containing the configuration data. """ cache_path: Path = config.get_brainglobe_dir() / "last_versions.conf" max_tries = 5 sleep_time = 0.5 status_code = 418 # teapot status code while max_tries > 0: try: result = requests.get(url) status_code = result.status_code if status_code == 200: break except requests.exceptions.ConnectionError: pass # keep trying to connect max_tries -= 1 sleep(sleep_time) if status_code != 200: print(f"Could not fetch the latest atlas versions: {status_code}") print(f"Using the last cached version from {cache_path}") return conf_from_file(cache_path) text = result.text config_obj = configparser.ConfigParser() config_obj.read_string(text) try: if not cache_path.parent.exists(): cache_path.parent.mkdir(parents=True, exist_ok=True) # Cache the available atlases with open(cache_path, "w") as f_out: config_obj.write(f_out) except OSError as e: print(f"Could not update the latest atlas versions cache: {e}") return config_obj
[docs] def conf_from_file(file_path: Path) -> configparser.ConfigParser: """Read a configuration file from a local file path. Parameters ---------- file_path : Path The path to the configuration file (e.g., obtained from config.get_brainglobe_dir()). Returns ------- configparser.ConfigParser A ConfigParser object containing the configuration data. Raises ------ FileNotFoundError If the specified `file_path` does not exist. """ if not file_path.exists(): raise FileNotFoundError("Last versions cache file not found.") with open(file_path, "r") as file: text = file.read() config = configparser.ConfigParser() config.read_string(text) return config
### File I/O
[docs] def read_json(path): """Read a json file. Parameters ---------- path : str or Path object Returns ------- dict Dictionary from the json """ with open(path, "r") as f: data = json.load(f) return data
[docs] def read_tiff(path): """Read a tiff file. Parameters ---------- path : str or Path object Returns ------- np.array Numpy stack read from the tiff. """ return tifffile.imread(str(path))