github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/python/aistore/sdk/utils.py (about)

     1  #
     2  # Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved.
     3  #
     4  from pathlib import Path
     5  from typing import Iterator, Type, TypeVar
     6  
     7  import braceexpand
     8  import humanize
     9  
    10  from msgspec import msgpack
    11  import pydantic.tools
    12  import requests
    13  from pydantic import BaseModel, parse_raw_as
    14  from requests import Response
    15  
    16  from aistore.sdk.const import UTF_ENCODING, HEADER_CONTENT_TYPE, MSGPACK_CONTENT_TYPE
    17  from aistore.sdk.errors import (
    18      AISError,
    19      ErrBckNotFound,
    20      ErrRemoteBckNotFound,
    21      ErrBckAlreadyExists,
    22      ErrETLAlreadyExists,
    23  )
    24  
    25  T = TypeVar("T")
    26  
    27  
    28  class HttpError(BaseModel):
    29      """
    30      Represents the errors returned by the API
    31      """
    32  
    33      status: int
    34      message: str = ""
    35      method: str = ""
    36      url_path: str = ""
    37      remote_addr: str = ""
    38      caller: str = ""
    39      node: str = ""
    40  
    41  
    42  def _raise_error(text: str):
    43      err = pydantic.tools.parse_raw_as(HttpError, text)
    44      if 400 <= err.status < 500:
    45          err = pydantic.tools.parse_raw_as(HttpError, text)
    46          if "does not exist" in err.message:
    47              if "cloud bucket" in err.message or "remote bucket" in err.message:
    48                  raise ErrRemoteBckNotFound(err.status, err.message)
    49              if "bucket" in err.message:
    50                  raise ErrBckNotFound(err.status, err.message)
    51          if "already exists" in err.message:
    52              if "bucket" in err.message:
    53                  raise ErrBckAlreadyExists(err.status, err.message)
    54              if "etl" in err.message:
    55                  raise ErrETLAlreadyExists(err.status, err.message)
    56      raise AISError(err.status, err.message)
    57  
    58  
    59  # pylint: disable=unused-variable
    60  def handle_errors(resp: requests.Response):
    61      """
    62      Error handling for requests made to the AIS Client
    63  
    64      Args:
    65          resp: requests.Response = Response received from the request
    66      """
    67      error_text = resp.text
    68      if isinstance(resp.text, bytes):
    69          try:
    70              error_text = error_text.decode(UTF_ENCODING)
    71          except UnicodeDecodeError:
    72              error_text = error_text.decode("iso-8859-1")
    73      if error_text != "":
    74          _raise_error(error_text)
    75      resp.raise_for_status()
    76  
    77  
    78  def probing_frequency(dur: int) -> float:
    79      """
    80      Given a timeout, return an interval to wait between retries
    81  
    82      Args:
    83          dur: Duration of timeout
    84  
    85      Returns:
    86          Frequency to probe
    87      """
    88      freq = min(dur / 8.0, 1.0)
    89      freq = max(dur / 64.0, freq)
    90      return max(freq, 0.1)
    91  
    92  
    93  def read_file_bytes(filepath: str):
    94      """
    95      Given a filepath, read the content as bytes
    96      Args:
    97          filepath: Existing local filepath
    98  
    99      Returns: Raw bytes
   100      """
   101      with open(filepath, "rb") as reader:
   102          return reader.read()
   103  
   104  
   105  def _check_path_exists(path: str):
   106      if not Path(path).exists():
   107          raise ValueError(f"Path: {path} does not exist")
   108  
   109  
   110  def validate_file(path: str):
   111      """
   112      Validate that a file exists and is a file
   113      Args:
   114          path: Path to validate
   115      Raises:
   116          ValueError: If path does not exist or is not a file
   117      """
   118      _check_path_exists(path)
   119      if not Path(path).is_file():
   120          raise ValueError(f"Path: {path} is a directory, not a file")
   121  
   122  
   123  def validate_directory(path: str):
   124      """
   125      Validate that a directory exists and is a directory
   126      Args:
   127          path: Path to validate
   128      Raises:
   129          ValueError: If path does not exist or is not a directory
   130      """
   131      _check_path_exists(path)
   132      if not Path(path).is_dir():
   133          raise ValueError(f"Path: {path} is a file, not a directory")
   134  
   135  
   136  def get_file_size(file: Path) -> str:
   137      """
   138      Get the size of a file and return it in human-readable format
   139      Args:
   140          file: File to read
   141  
   142      Returns:
   143          Size of file as human-readable string
   144  
   145      """
   146      return (
   147          humanize.naturalsize(file.stat().st_size) if file.stat().st_size else "unknown"
   148      )
   149  
   150  
   151  def expand_braces(template: str) -> Iterator[str]:
   152      """
   153      Given a string template, apply bash-style brace expansion to return a list of strings
   154      Args:
   155          template: Valid brace expansion input, e.g. prefix-{0..10..2}-gap-{11..15}-suffix
   156  
   157      Returns:
   158          Iterator of brace expansion output
   159  
   160      """
   161      # pylint: disable = fixme
   162      # TODO Build custom expansion to validate consistent with cmn/cos/template.go TemplateRange
   163      return braceexpand.braceexpand(template)
   164  
   165  
   166  def decode_response(
   167      res_model: Type[T],
   168      resp: Response,
   169  ) -> T:
   170      """
   171      Parse response content from the cluster into a Python class,
   172       decoding with msgpack depending on content type in header
   173  
   174      Args:
   175          res_model (Type[T]): Resulting type to which the response should be deserialized
   176          resp (Response): Response from the AIS cluster
   177  
   178      """
   179      if resp.headers.get(HEADER_CONTENT_TYPE) == MSGPACK_CONTENT_TYPE:
   180          return msgpack.decode(resp.content, type=res_model)
   181      return parse_raw_as(res_model, resp.text)