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)