Source code for sentinelhub.download.handlers

"""
Module implementing error handlers which can occur during download procedure
"""
import functools
import logging
import time
from typing import Callable, Optional, TypeVar

import requests
from typing_extensions import Protocol

from ..config import SHConfig
from ..decoding import decode_sentinelhub_err_msg
from ..exceptions import DownloadFailedException
from .models import DownloadRequest


class _HasConfig(Protocol):
    """Interface of objects with a config."""

    config: SHConfig


Self = TypeVar("Self")
SelfWithConfig = TypeVar("SelfWithConfig", bound=_HasConfig)
T = TypeVar("T")


LOGGER = logging.getLogger(__name__)


[docs]def fail_user_errors(download_func: Callable[[Self, DownloadRequest], T]) -> Callable[[Self, DownloadRequest], T]: """Decorator function for handling user errors""" @functools.wraps(download_func) def new_download_func(self: Self, request: DownloadRequest) -> T: try: return download_func(self, request) except requests.HTTPError as exception: if ( exception.response.status_code < requests.status_codes.codes.INTERNAL_SERVER_ERROR and exception.response.status_code != requests.status_codes.codes.TOO_MANY_REQUESTS ): raise DownloadFailedException( _create_download_failed_message(exception, request.url), request_exception=exception ) from exception raise exception from exception return new_download_func
[docs]def retry_temporary_errors( download_func: Callable[[SelfWithConfig, DownloadRequest], T] ) -> Callable[[SelfWithConfig, DownloadRequest], T]: """Decorator function for handling server and connection errors""" backoff_coefficient = 3 @functools.wraps(download_func) def new_download_func(self: SelfWithConfig, request: DownloadRequest) -> T: download_attempts = self.config.max_download_attempts sleep_time = self.config.download_sleep_time for attempt_idx in range(download_attempts): try: return download_func(self, request) except requests.RequestException as exception: attempts_left = download_attempts - (attempt_idx + 1) if not ( _is_temporary_problem(exception) or ( isinstance(exception, requests.HTTPError) and exception.response.status_code >= requests.status_codes.codes.INTERNAL_SERVER_ERROR ) ): raise exception from exception if attempts_left <= 0: message = _create_download_failed_message(exception, request.url) raise DownloadFailedException(message, request_exception=exception) from exception LOGGER.debug( "Download attempt failed: %s\n%d attempts left, will retry in %ds", exception, attempts_left, sleep_time, ) time.sleep(sleep_time) sleep_time *= backoff_coefficient raise DownloadFailedException( "No download attempts available - configuration parameter max_download_attempts should be greater than 0" ) return new_download_func
[docs]def fail_missing_file(download_func: Callable[[Self, DownloadRequest], T]) -> Callable[[Self, DownloadRequest], T]: """A decorator for raising an error if a file is missing""" @functools.wraps(download_func) def new_download_func(self: Self, request: DownloadRequest) -> T: try: return download_func(self, request) except requests.HTTPError as exception: if exception.response.status_code == requests.status_codes.codes.NOT_FOUND: raise DownloadFailedException( f"File in location {request.url} is missing", request_exception=exception ) from exception raise exception from exception return new_download_func
def _is_temporary_problem(exception: Exception) -> bool: """Checks if the obtained exception is temporary and if download attempt should be repeated :param exception: Exception raised during download :return: `True` if exception is temporary and `False` otherwise """ return isinstance(exception, (requests.ConnectionError, requests.Timeout, requests.exceptions.ChunkedEncodingError)) def _create_download_failed_message(exception: Exception, url: Optional[str]) -> str: """Creates message describing why download has failed :param exception: Exception raised during download :param url: A URL from where download was attempted :return: Error message """ message = f"Failed to download from:\n{url}\nwith {exception.__class__.__name__}:\n{exception}" if _is_temporary_problem(exception): if isinstance(exception, requests.ConnectionError): message += "\nPlease check your internet connection and try again." else: message += ( "\nThere might be a problem in connection or the server failed to process " "your request. Please try again." ) elif isinstance(exception, requests.HTTPError): server_message = decode_sentinelhub_err_msg(exception.response) message += f'\nServer response: "{server_message}"' return message