1+ import json
12import logging
3+ import typing
24from typing import Any , BinaryIO , Optional , Tuple , Union
35from urllib .parse import urlparse
46
57import requests
6- import json
78from requests import Response
89
9- from brainframe .api .bf_codecs import Codec
10- from brainframe .api .bf_errors import InvalidSessionError , UnknownError , \
11- kind_to_error_type
10+ from brainframe .api import bf_codecs , bf_errors
1211
1312DEFAULT_TIMEOUT = 30
1413"""The default timeout for most requests."""
@@ -76,7 +75,7 @@ def _put_json(self, api_url, timeout, json_data):
7675 return json .loads (resp .content )
7776 return None
7877
79- def _post_codec (self , api_url , timeout , codec : Codec ):
78+ def _post_codec (self , api_url , timeout , codec : bf_codecs . Codec ):
8079 """Send a POST request to the given URL.
8180
8281 :param api_url: The /api/blah/blah to append to the base_url
@@ -272,33 +271,29 @@ def _send_authorized(self, request: requests.Request, timeout) \
272271 """
273272 if self ._credentials is None :
274273 # No credentials provided, send the request without any auth
275- resp = self ._send_no_auth ( request , timeout )
274+ send_func = self ._send_no_auth
276275 elif self ._session_id is None :
277276 # Authenticate with username and password to get a new session ID
278- resp = self ._send_with_credentials ( request , timeout )
277+ send_func = self ._send_with_credentials
279278 else :
280279 # Authenticate with the session ID
281- resp = self ._send_with_session_id ( request , timeout )
280+ send_func = self ._send_with_session_id
282281
282+ resp = send_func (request , timeout )
283283 return resp
284284
285285 def _send_no_auth (self , request : requests .Request , timeout ) \
286286 -> requests .Response :
287287 """Sends the given request with no authorization."""
288288 resp = self ._send_request (request , timeout )
289- if not resp .ok :
290- raise _make_api_error (resp .content , resp .status_code )
291-
292289 return resp
293290
294291 def _send_with_credentials (self , request : requests .Request , timeout ) \
295292 -> requests .Response :
296293 """Sends the given request with HTTP Basic Authorization."""
297- request .auth = self ._credentials
298294
295+ request .auth = self ._credentials
299296 resp = self ._send_request (request , timeout )
300- if not resp .ok :
301- raise _make_api_error (resp .content , resp .status_code )
302297
303298 if "session_id" in resp .cookies :
304299 # Update the session ID if we don't already have one
@@ -310,12 +305,9 @@ def _send_with_session_id(self, request: requests.Request, timeout) \
310305 -> requests .Response :
311306 """Sends the given request with the session ID."""
312307 request .cookies = {"session_id" : self ._session_id }
313-
314308 try :
315309 resp = self ._send_request (request , timeout )
316- if not resp .ok :
317- raise _make_api_error (resp .content , resp .status_code )
318- except InvalidSessionError :
310+ except bf_errors .InvalidSessionError :
319311 # The session likely expired. Try again with the username and
320312 # password to fetch a new session
321313 request .cookies = None
@@ -333,38 +325,102 @@ def _send_request(request: requests.Request, timeout: int) \
333325 :return: The response data
334326 """
335327 prepared = request .prepare ()
336- return requests .Session ().send (prepared , stream = True , timeout = timeout )
328+
329+ session = requests .Session ()
330+ try :
331+ resp = session .send (prepared , stream = True , timeout = timeout )
332+ except requests .exceptions .RequestException as exc :
333+ raise _make_api_error (exception = exc )
334+
335+ if not resp .ok :
336+ raise _make_api_error (resp = resp )
337+
338+ return resp
337339
338340
339- def _make_api_error (resp_content , status_code ):
341+ @typing .overload
342+ def _make_api_error (exception : Exception ) -> bf_errors .BaseAPIError :
343+ ...
344+
345+
346+ @typing .overload
347+ def _make_api_error (resp : requests .Response ) -> bf_errors .BaseAPIError :
348+ ...
349+
350+
351+ def _make_api_error (resp : requests .Response = None ,
352+ exception : BaseException = None ) \
353+ -> bf_errors .BaseAPIError :
340354 """Makes the corresponding error for this response.
341355
342- :param resp_content: The HTTP response to inspect for info
356+ Pass either a requests.Response with a status code, or an exception
357+
358+ :param resp: The HTTP response to inspect for info
359+ :param exception: An exception during the request
343360 :return: An error that can be thrown describing this failure
344361 """
345- if len (resp_content ) == 0 :
346- kind = UnknownError .kind
347- description = ("A failure happened but the server did not respond "
348- "with a proper error" )
349- else :
350- try :
351- resp_content = json .loads (resp_content )
352- kind = resp_content ["title" ]
353- description = resp_content ["description" ]
354- except ValueError :
355- # The content of the error was not in the proper format. This might
356- # happen if some part of our request handling pipeline failed that
357- # doesn't know about our error handling format. Not ideal.
358- kind = UnknownError .kind
359- resp_content = resp_content .decode ("utf-8" )
360- description = ("A failure happened, and the response was not in "
361- "the proper error format: " + resp_content )
362-
363- if kind not in kind_to_error_type :
364- info = f"Unknown error kind { kind } : " + description
365- logging .error (info )
366- return UnknownError (info , status_code )
362+
363+ server_not_ready_msg = "A network exception occurred while communicating " \
364+ "with the BrainFrame server"
365+
366+ if resp is not None :
367+ if len (resp .content ) == 0 :
368+ description = ("A failure happened but the server did not respond "
369+ "with a proper error" )
370+ return bf_errors .UnknownError (description )
371+ else :
372+ resp_content = resp .content .decode ("utf-8" )
373+
374+ # This is here to catch the nginx error that can occur as the server
375+ # starts up
376+ if resp .status_code == 502 :
377+ description = f"{ server_not_ready_msg } : { resp_content } "
378+ return bf_errors .ServerNotReadyError (description )
379+
380+ try :
381+ resp_content = json .loads (resp .content )
382+ kind = resp_content ["title" ]
383+ description = resp_content ["description" ]
384+ except ValueError :
385+ # The content of the error was not in the proper format. This
386+ # might happen if some part of our request handling pipeline
387+ # failed that doesn't know about our error handling format. Not
388+ # ideal.
389+ exc_type = bf_errors .UnknownError
390+ description = (
391+ f"A failure happened, and the response was not in "
392+ f"the proper error format: { resp_content } "
393+ )
394+ else :
395+ try :
396+ exc_type = bf_errors .kind_to_error_type [kind ]
397+ except KeyError :
398+ description = f"Unknown error kind { kind } : { description } "
399+ logging .error (description )
400+ exc_type = bf_errors .UnknownError
401+
402+ if exc_type is bf_errors .UnknownError :
403+ return exc_type (description , resp .status_code )
404+ else :
405+ return exc_type (description )
406+
407+ elif exception is not None :
408+ if isinstance (exception , requests .exceptions .RequestException ):
409+ exc_type = bf_errors .ServerNotReadyError
410+ description = server_not_ready_msg
411+ else :
412+ exc_type = bf_errors .UnknownError
413+ description = "An unknown network exception occurred while " \
414+ "attempting to communicate with the BrainFrame server"
415+
416+ new_exc = exc_type (description )
417+ # This is identical to `raise x from y`, but does not immediately raise
418+ new_exc .__cause__ = exception
419+ return new_exc
420+
367421 else :
368- if kind == UnknownError .kind :
369- return kind_to_error_type [kind ](description , status_code )
370- return kind_to_error_type [kind ](description )
422+ description = f"{ _make_api_error .__name__ } called without arguments"
423+ new_exc = ValueError (description )
424+ if exception is not None :
425+ new_exc .__cause__ = exception
426+ raise new_exc
0 commit comments