Coretex
network_response.py
1 # Copyright (C) 2023 Coretex LLC
2 
3 # This file is part of Coretex.ai
4 
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Affero General Public License for more details.
14 
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program. If not, see <https://www.gnu.org/licenses/>.
17 
18 from typing import Union, Type, TypeVar, Optional, Iterator, Any
19 from http import HTTPStatus
20 
21 from requests import Response
22 from requests.structures import CaseInsensitiveDict
23 
24 from .request_type import RequestType
25 
26 
27 JsonType = TypeVar("JsonType", bound = Union[list, dict])
28 
29 
31 
32  """
33  Represents Coretex backend response to network request
34 
35  Properties
36  ----------
37  response : Response
38  python.requests HTTP reponse
39  endpoint : str
40  endpoint to which the request was sent
41  """
42 
43  def __init__(self, response: Response, endpoint: str):
44  self._raw_raw = response
45  self.endpointendpoint = endpoint
46 
47  @property
48  def statusCode(self) -> int:
49  """
50  Status code of the HTTP response
51  """
52 
53  return self._raw_raw.status_code
54 
55  @property
56  def headers(self) -> CaseInsensitiveDict:
57  """
58  HTTP Response headers
59  """
60 
61  return self._raw_raw.headers
62 
63  def hasFailed(self) -> bool:
64  """
65  Checks if request has failed
66 
67  Returns
68  -------
69  bool -> True if request has failed, False if request has not failed
70  """
71 
72  return not self._raw_raw.ok
73 
74  def isUnauthorized(self) -> bool:
75  """
76  Checks if request was unauthorized
77 
78  Returns
79  -------
80  bool -> True if status code is 401 and request has failed, False if not
81  """
82 
83  return self.statusCodestatusCodestatusCode == HTTPStatus.UNAUTHORIZED and self.hasFailedhasFailed()
84 
85  def isHead(self) -> bool:
86  """
87  Returns
88  bool -> True if the request method which created this reponse is HEAD, False if not
89  """
90 
91  return self._raw_raw.request.method == RequestType.head.value
92 
93  def getJson(self, type_: Type[JsonType]) -> JsonType:
94  """
95  Converts HTTP response body to json
96 
97  Parameters
98  ----------
99  type_: Type[JsonType]
100  list or dict types to which the json should be cast
101 
102  Returns
103  -------
104  JsonType -> Either a list or a dict object depending on type_ parameter
105 
106  Raises
107  ------
108  ValueError -> If "Content-Type" header was not "application/json"
109  TypeError -> If it was not possible to convert body to type of passed "type_" parameter
110  """
111 
112  if not "application/json" in self.headersheaders.get("Content-Type", ""):
113  raise ValueError(f">> [Coretex] Trying to convert request response to json but response \"Content-Type\" was \"{self.headers.get('Content-Type')}\"")
114 
115  value = self._raw_raw.json()
116  if not isinstance(value, type_):
117  raise TypeError(f">> [Coretex] Expected json response to be of type \"{type_.__name__}\", received \"{type(value).__name__}\"")
118 
119  return value
120 
121  def getContent(self) -> bytes:
122  """
123  Returns
124  -------
125  bytes -> body of the request as bytes
126  """
127 
128  return self._raw_raw.content
129 
130  def stream(self, chunkSize: Optional[int] = 1, decodeUnicode: bool = False) -> Iterator[Any]:
131  """
132  Downloads HTTP response in chunks and returns them as they are being downloaded
133 
134  Parameters
135  ----------
136  chunkSize : Optional[int]
137  A value of None will function differently depending on the value of stream.
138  stream = True will read data as it arrives in whatever size the chunks are
139  received. If stream = False, data is returned as a single chunk.
140  decodeUnicode : bool
141  If decode_unicode is True, content will be decoded using the best
142  available encoding based on the response
143 
144  Returns
145  -------
146  Iterator[Any] -> HTTP response as chunks
147  """
148 
149  return self._raw_raw.iter_content(chunkSize, decodeUnicode)
150 
151 
152 class NetworkRequestError(Exception):
153 
154  """
155  Exception which is raised when an request fails.
156  Request is marked as failed when the http code is: >= 400
157  """
158 
159  def __init__(self, response: NetworkResponse, message: str) -> None:
160  if not response.hasFailed():
161  raise ValueError(">> [Coretex] Invalid request response")
162 
163  try:
164  # This will raise ValueError if response is not of type application/json
165  # which is the case for response (mostly errors) returned by nginx which are html
166  responseJson = response.getJson(dict)
167 
168  if "message" in responseJson:
169  responseMessage = responseJson["message"]
170  else:
171  responseMessage = response._raw.content.decode()
172  except ValueError:
173  responseMessage = response._raw.content.decode()
174 
175  super().__init__(f">> [Coretex] {message}. Reason: {responseMessage}")
176 
177  self.responseresponse = response
JsonType getJson(self, Type[JsonType] type_)
Iterator[Any] stream(self, Optional[int] chunkSize=1, bool decodeUnicode=False)