From 98a7fb9bd778cd9f8cb1cdeba2f728e06a85279f Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 30 Sep 2021 15:04:56 -0700 Subject: [PATCH] fix: Extracting GAPIC API calls into a new module --- firebase_admin/_gapic_utils.py | 122 +++++++++++++++++++++++++++++++++ firebase_admin/_utils.py | 101 --------------------------- firebase_admin/messaging.py | 3 +- tests/test_exceptions.py | 29 ++++---- 4 files changed, 140 insertions(+), 115 deletions(-) create mode 100644 firebase_admin/_gapic_utils.py diff --git a/firebase_admin/_gapic_utils.py b/firebase_admin/_gapic_utils.py new file mode 100644 index 000000000..3c975808c --- /dev/null +++ b/firebase_admin/_gapic_utils.py @@ -0,0 +1,122 @@ +# Copyright 2021 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Internal utilities for interacting with Google API client.""" + +import io +import socket + +import googleapiclient +import httplib2 +import requests + +from firebase_admin import exceptions +from firebase_admin import _utils + + +def handle_platform_error_from_googleapiclient(error, handle_func=None): + """Constructs a ``FirebaseError`` from the given googleapiclient error. + + This can be used to handle errors returned by Google Cloud Platform (GCP) APIs. + + Args: + error: An error raised by the googleapiclient while making an HTTP call to a GCP API. + handle_func: A function that can be used to handle platform errors in a custom way. When + specified, this function will be called with three arguments. It has the same + signature as ```_handle_func_googleapiclient``, but may return ``None``. + + Returns: + FirebaseError: A ``FirebaseError`` that can be raised to the user code. + """ + if not isinstance(error, googleapiclient.errors.HttpError): + return handle_googleapiclient_error(error) + + content = error.content.decode() + status_code = error.resp.status + error_dict, message = _utils._parse_platform_error(content, status_code) # pylint: disable=protected-access + http_response = _http_response_from_googleapiclient_error(error) + exc = None + if handle_func: + exc = handle_func(error, message, error_dict, http_response) + + return exc if exc else _handle_func_googleapiclient(error, message, error_dict, http_response) + + +def _handle_func_googleapiclient(error, message, error_dict, http_response): + """Constructs a ``FirebaseError`` from the given GCP error. + + Args: + error: An error raised by the googleapiclient module while making an HTTP call. + message: A message to be included in the resulting ``FirebaseError``. + error_dict: Parsed GCP error response. + http_response: A requests HTTP response object to associate with the exception. + + Returns: + FirebaseError: A ``FirebaseError`` that can be raised to the user code or None. + """ + code = error_dict.get('status') + return handle_googleapiclient_error(error, message, code, http_response) + + +def handle_googleapiclient_error(error, message=None, code=None, http_response=None): + """Constructs a ``FirebaseError`` from the given googleapiclient error. + + This method is agnostic of the remote service that produced the error, whether it is a GCP + service or otherwise. Therefore, this method does not attempt to parse the error response in + any way. + + Args: + error: An error raised by the googleapiclient module while making an HTTP call. + message: A message to be included in the resulting ``FirebaseError`` (optional). If not + specified the string representation of the ``error`` argument is used as the message. + code: A GCP error code that will be used to determine the resulting error type (optional). + If not specified the HTTP status code on the error response is used to determine a + suitable error code. + http_response: A requests HTTP response object to associate with the exception (optional). + If not specified, one will be created from the ``error``. + + Returns: + FirebaseError: A ``FirebaseError`` that can be raised to the user code. + """ + if isinstance(error, socket.timeout) or ( + isinstance(error, socket.error) and 'timed out' in str(error)): + return exceptions.DeadlineExceededError( + message='Timed out while making an API call: {0}'.format(error), + cause=error) + if isinstance(error, httplib2.ServerNotFoundError): + return exceptions.UnavailableError( + message='Failed to establish a connection: {0}'.format(error), + cause=error) + if not isinstance(error, googleapiclient.errors.HttpError): + return exceptions.UnknownError( + message='Unknown error while making a remote service call: {0}'.format(error), + cause=error) + + if not code: + code = _utils._http_status_to_error_code(error.resp.status) # pylint: disable=protected-access + if not message: + message = str(error) + if not http_response: + http_response = _http_response_from_googleapiclient_error(error) + + err_type = _utils._error_code_to_exception_type(code) # pylint: disable=protected-access + return err_type(message=message, cause=error, http_response=http_response) + + +def _http_response_from_googleapiclient_error(error): + """Creates a requests HTTP Response object from the given googleapiclient error.""" + resp = requests.models.Response() + resp.raw = io.BytesIO(error.content) + resp.status_code = error.resp.status + return resp diff --git a/firebase_admin/_utils.py b/firebase_admin/_utils.py index 8c640276c..dcfb520d2 100644 --- a/firebase_admin/_utils.py +++ b/firebase_admin/_utils.py @@ -14,13 +14,9 @@ """Internal utilities common to all modules.""" -import io import json -import socket import google.auth -import googleapiclient -import httplib2 import requests import firebase_admin @@ -206,103 +202,6 @@ def handle_requests_error(error, message=None, code=None): return err_type(message=message, cause=error, http_response=error.response) -def handle_platform_error_from_googleapiclient(error, handle_func=None): - """Constructs a ``FirebaseError`` from the given googleapiclient error. - - This can be used to handle errors returned by Google Cloud Platform (GCP) APIs. - - Args: - error: An error raised by the googleapiclient while making an HTTP call to a GCP API. - handle_func: A function that can be used to handle platform errors in a custom way. When - specified, this function will be called with three arguments. It has the same - signature as ```_handle_func_googleapiclient``, but may return ``None``. - - Returns: - FirebaseError: A ``FirebaseError`` that can be raised to the user code. - """ - if not isinstance(error, googleapiclient.errors.HttpError): - return handle_googleapiclient_error(error) - - content = error.content.decode() - status_code = error.resp.status - error_dict, message = _parse_platform_error(content, status_code) - http_response = _http_response_from_googleapiclient_error(error) - exc = None - if handle_func: - exc = handle_func(error, message, error_dict, http_response) - - return exc if exc else _handle_func_googleapiclient(error, message, error_dict, http_response) - - -def _handle_func_googleapiclient(error, message, error_dict, http_response): - """Constructs a ``FirebaseError`` from the given GCP error. - - Args: - error: An error raised by the googleapiclient module while making an HTTP call. - message: A message to be included in the resulting ``FirebaseError``. - error_dict: Parsed GCP error response. - http_response: A requests HTTP response object to associate with the exception. - - Returns: - FirebaseError: A ``FirebaseError`` that can be raised to the user code or None. - """ - code = error_dict.get('status') - return handle_googleapiclient_error(error, message, code, http_response) - - -def handle_googleapiclient_error(error, message=None, code=None, http_response=None): - """Constructs a ``FirebaseError`` from the given googleapiclient error. - - This method is agnostic of the remote service that produced the error, whether it is a GCP - service or otherwise. Therefore, this method does not attempt to parse the error response in - any way. - - Args: - error: An error raised by the googleapiclient module while making an HTTP call. - message: A message to be included in the resulting ``FirebaseError`` (optional). If not - specified the string representation of the ``error`` argument is used as the message. - code: A GCP error code that will be used to determine the resulting error type (optional). - If not specified the HTTP status code on the error response is used to determine a - suitable error code. - http_response: A requests HTTP response object to associate with the exception (optional). - If not specified, one will be created from the ``error``. - - Returns: - FirebaseError: A ``FirebaseError`` that can be raised to the user code. - """ - if isinstance(error, socket.timeout) or ( - isinstance(error, socket.error) and 'timed out' in str(error)): - return exceptions.DeadlineExceededError( - message='Timed out while making an API call: {0}'.format(error), - cause=error) - if isinstance(error, httplib2.ServerNotFoundError): - return exceptions.UnavailableError( - message='Failed to establish a connection: {0}'.format(error), - cause=error) - if not isinstance(error, googleapiclient.errors.HttpError): - return exceptions.UnknownError( - message='Unknown error while making a remote service call: {0}'.format(error), - cause=error) - - if not code: - code = _http_status_to_error_code(error.resp.status) - if not message: - message = str(error) - if not http_response: - http_response = _http_response_from_googleapiclient_error(error) - - err_type = _error_code_to_exception_type(code) - return err_type(message=message, cause=error, http_response=http_response) - - -def _http_response_from_googleapiclient_error(error): - """Creates a requests HTTP Response object from the given googleapiclient error.""" - resp = requests.models.Response() - resp.raw = io.BytesIO(error.content) - resp.status_code = error.resp.status - return resp - - def _http_status_to_error_code(status): """Maps an HTTP status to a platform error code.""" return _HTTP_STATUS_TO_ERROR_CODE.get(status, exceptions.UNKNOWN) diff --git a/firebase_admin/messaging.py b/firebase_admin/messaging.py index 95fc03e04..46dd7d410 100644 --- a/firebase_admin/messaging.py +++ b/firebase_admin/messaging.py @@ -24,6 +24,7 @@ from firebase_admin import _http_client from firebase_admin import _messaging_encoder from firebase_admin import _messaging_utils +from firebase_admin import _gapic_utils from firebase_admin import _utils @@ -466,7 +467,7 @@ def _handle_iid_error(self, error): def _handle_batch_error(self, error): """Handles errors received from the googleapiclient while making batch requests.""" - return _utils.handle_platform_error_from_googleapiclient( + return _gapic_utils.handle_platform_error_from_googleapiclient( error, _MessagingService._build_fcm_error_googleapiclient) @classmethod diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 96072d91b..4347c838a 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -24,6 +24,7 @@ from googleapiclient import errors from firebase_admin import exceptions from firebase_admin import _utils +from firebase_admin import _gapic_utils _NOT_FOUND_ERROR_DICT = { @@ -186,7 +187,7 @@ class TestGoogleApiClient: socket.error('Read timed out') ]) def test_googleapicleint_timeout_error(self, error): - firebase_error = _utils.handle_googleapiclient_error(error) + firebase_error = _gapic_utils.handle_googleapiclient_error(error) assert isinstance(firebase_error, exceptions.DeadlineExceededError) assert str(firebase_error) == 'Timed out while making an API call: {0}'.format(error) assert firebase_error.cause is error @@ -194,7 +195,7 @@ def test_googleapicleint_timeout_error(self, error): def test_googleapiclient_connection_error(self): error = httplib2.ServerNotFoundError('Test error') - firebase_error = _utils.handle_googleapiclient_error(error) + firebase_error = _gapic_utils.handle_googleapiclient_error(error) assert isinstance(firebase_error, exceptions.UnavailableError) assert str(firebase_error) == 'Failed to establish a connection: Test error' assert firebase_error.cause is error @@ -202,7 +203,7 @@ def test_googleapiclient_connection_error(self): def test_unknown_transport_error(self): error = socket.error('Test error') - firebase_error = _utils.handle_googleapiclient_error(error) + firebase_error = _gapic_utils.handle_googleapiclient_error(error) assert isinstance(firebase_error, exceptions.UnknownError) assert str(firebase_error) == 'Unknown error while making a remote service call: Test error' assert firebase_error.cause is error @@ -210,7 +211,7 @@ def test_unknown_transport_error(self): def test_http_response(self): error = self._create_http_error() - firebase_error = _utils.handle_googleapiclient_error(error) + firebase_error = _gapic_utils.handle_googleapiclient_error(error) assert isinstance(firebase_error, exceptions.InternalError) assert str(firebase_error) == str(error) assert firebase_error.cause is error @@ -219,7 +220,7 @@ def test_http_response(self): def test_http_response_with_unknown_status(self): error = self._create_http_error(status=501) - firebase_error = _utils.handle_googleapiclient_error(error) + firebase_error = _gapic_utils.handle_googleapiclient_error(error) assert isinstance(firebase_error, exceptions.UnknownError) assert str(firebase_error) == str(error) assert firebase_error.cause is error @@ -228,7 +229,7 @@ def test_http_response_with_unknown_status(self): def test_http_response_with_message(self): error = self._create_http_error() - firebase_error = _utils.handle_googleapiclient_error( + firebase_error = _gapic_utils.handle_googleapiclient_error( error, message='Explicit error message') assert isinstance(firebase_error, exceptions.InternalError) assert str(firebase_error) == 'Explicit error message' @@ -238,7 +239,7 @@ def test_http_response_with_message(self): def test_http_response_with_code(self): error = self._create_http_error() - firebase_error = _utils.handle_googleapiclient_error( + firebase_error = _gapic_utils.handle_googleapiclient_error( error, code=exceptions.UNAVAILABLE) assert isinstance(firebase_error, exceptions.UnavailableError) assert str(firebase_error) == str(error) @@ -248,7 +249,7 @@ def test_http_response_with_code(self): def test_http_response_with_message_and_code(self): error = self._create_http_error() - firebase_error = _utils.handle_googleapiclient_error( + firebase_error = _gapic_utils.handle_googleapiclient_error( error, message='Explicit error message', code=exceptions.UNAVAILABLE) assert isinstance(firebase_error, exceptions.UnavailableError) assert str(firebase_error) == 'Explicit error message' @@ -258,7 +259,7 @@ def test_http_response_with_message_and_code(self): def test_handle_platform_error(self): error = self._create_http_error(payload=_NOT_FOUND_PAYLOAD) - firebase_error = _utils.handle_platform_error_from_googleapiclient(error) + firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient(error) assert isinstance(firebase_error, exceptions.NotFoundError) assert str(firebase_error) == 'test error' assert firebase_error.cause is error @@ -267,7 +268,7 @@ def test_handle_platform_error(self): def test_handle_platform_error_with_no_response(self): error = socket.error('Test error') - firebase_error = _utils.handle_platform_error_from_googleapiclient(error) + firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient(error) assert isinstance(firebase_error, exceptions.UnknownError) assert str(firebase_error) == 'Unknown error while making a remote service call: Test error' assert firebase_error.cause is error @@ -275,7 +276,7 @@ def test_handle_platform_error_with_no_response(self): def test_handle_platform_error_with_no_error_code(self): error = self._create_http_error(payload='no error code') - firebase_error = _utils.handle_platform_error_from_googleapiclient(error) + firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient(error) assert isinstance(firebase_error, exceptions.InternalError) message = 'Unexpected HTTP response with status: 500; body: no error code' assert str(firebase_error) == message @@ -291,7 +292,8 @@ def _custom_handler(cause, message, error_dict, http_response): invocations.append((cause, message, error_dict, http_response)) return exceptions.InvalidArgumentError('Custom message', cause, http_response) - firebase_error = _utils.handle_platform_error_from_googleapiclient(error, _custom_handler) + firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient( + error, _custom_handler) assert isinstance(firebase_error, exceptions.InvalidArgumentError) assert str(firebase_error) == 'Custom message' @@ -313,7 +315,8 @@ def test_handle_platform_error_with_custom_handler_ignore(self): def _custom_handler(cause, message, error_dict, http_response): invocations.append((cause, message, error_dict, http_response)) - firebase_error = _utils.handle_platform_error_from_googleapiclient(error, _custom_handler) + firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient( + error, _custom_handler) assert isinstance(firebase_error, exceptions.NotFoundError) assert str(firebase_error) == 'test error'