Skip to content

fix: Extracting GAPIC API calls into a new module #581

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions firebase_admin/_gapic_utils.py
Original file line number Diff line number Diff line change
@@ -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
101 changes: 0 additions & 101 deletions firebase_admin/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion firebase_admin/messaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down
29 changes: 16 additions & 13 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -186,31 +187,31 @@ 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
assert firebase_error.http_response is None

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
assert firebase_error.http_response is None

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
assert firebase_error.http_response is None

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
Expand All @@ -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
Expand All @@ -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'
Expand All @@ -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)
Expand All @@ -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'
Expand All @@ -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
Expand All @@ -267,15 +268,15 @@ 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
assert firebase_error.http_response is None

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
Expand All @@ -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'
Expand All @@ -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'
Expand Down