Skip to content

Commit 3361452

Browse files
authored
Project management API migrated to new error types (#314)
1 parent 29c8b7a commit 3361452

File tree

3 files changed

+142
-149
lines changed

3 files changed

+142
-149
lines changed

firebase_admin/project_management.py

Lines changed: 32 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import six
2626

2727
import firebase_admin
28+
from firebase_admin import exceptions
2829
from firebase_admin import _http_client
2930
from firebase_admin import _utils
3031

@@ -139,21 +140,6 @@ def _check_not_none(obj, field_name):
139140
return obj
140141

141142

142-
class ApiCallError(Exception):
143-
"""An error encountered while interacting with the Firebase Project Management Service."""
144-
145-
def __init__(self, message, error):
146-
Exception.__init__(self, message)
147-
self.detail = error
148-
149-
150-
class _PollingError(Exception):
151-
"""An error encountered during the polling of an app's creation status."""
152-
153-
def __init__(self, message):
154-
Exception.__init__(self, message)
155-
156-
157143
class AndroidApp(object):
158144
"""A reference to an Android app within a Firebase project.
159145
@@ -185,7 +171,7 @@ def get_metadata(self):
185171
AndroidAppMetadata: An ``AndroidAppMetadata`` instance.
186172
187173
Raises:
188-
ApiCallError: If an error occurs while communicating with the Firebase Project
174+
FirebaseError: If an error occurs while communicating with the Firebase Project
189175
Management Service.
190176
"""
191177
return self._service.get_android_app_metadata(self._app_id)
@@ -200,7 +186,7 @@ def set_display_name(self, new_display_name):
200186
NoneType: None.
201187
202188
Raises:
203-
ApiCallError: If an error occurs while communicating with the Firebase Project
189+
FirebaseError: If an error occurs while communicating with the Firebase Project
204190
Management Service.
205191
"""
206192
return self._service.set_android_app_display_name(self._app_id, new_display_name)
@@ -216,7 +202,7 @@ def get_sha_certificates(self):
216202
list: A list of ``ShaCertificate`` instances.
217203
218204
Raises:
219-
ApiCallError: If an error occurs while communicating with the Firebase Project
205+
FirebaseError: If an error occurs while communicating with the Firebase Project
220206
Management Service.
221207
"""
222208
return self._service.get_sha_certificates(self._app_id)
@@ -231,7 +217,7 @@ def add_sha_certificate(self, certificate_to_add):
231217
NoneType: None.
232218
233219
Raises:
234-
ApiCallError: If an error occurs while communicating with the Firebase Project
220+
FirebaseError: If an error occurs while communicating with the Firebase Project
235221
Management Service. (For example, if the certificate_to_add already exists.)
236222
"""
237223
return self._service.add_sha_certificate(self._app_id, certificate_to_add)
@@ -246,7 +232,7 @@ def delete_sha_certificate(self, certificate_to_delete):
246232
NoneType: None.
247233
248234
Raises:
249-
ApiCallError: If an error occurs while communicating with the Firebase Project
235+
FirebaseError: If an error occurs while communicating with the Firebase Project
250236
Management Service. (For example, if the certificate_to_delete is not found.)
251237
"""
252238
return self._service.delete_sha_certificate(certificate_to_delete)
@@ -283,7 +269,7 @@ def get_metadata(self):
283269
IosAppMetadata: An ``IosAppMetadata`` instance.
284270
285271
Raises:
286-
ApiCallError: If an error occurs while communicating with the Firebase Project
272+
FirebaseError: If an error occurs while communicating with the Firebase Project
287273
Management Service.
288274
"""
289275
return self._service.get_ios_app_metadata(self._app_id)
@@ -298,7 +284,7 @@ def set_display_name(self, new_display_name):
298284
NoneType: None.
299285
300286
Raises:
301-
ApiCallError: If an error occurs while communicating with the Firebase Project
287+
FirebaseError: If an error occurs while communicating with the Firebase Project
302288
Management Service.
303289
"""
304290
return self._service.set_ios_app_display_name(self._app_id, new_display_name)
@@ -478,22 +464,11 @@ class _ProjectManagementService(object):
478464
MAXIMUM_POLLING_ATTEMPTS = 8
479465
POLL_BASE_WAIT_TIME_SECONDS = 0.5
480466
POLL_EXPONENTIAL_BACKOFF_FACTOR = 1.5
481-
ERROR_CODES = {
482-
401: 'Request not authorized.',
483-
403: 'Client does not have sufficient privileges.',
484-
404: 'Failed to find the resource.',
485-
409: 'The resource already exists.',
486-
429: 'Request throttled out by the backend server.',
487-
500: 'Internal server error.',
488-
503: 'Backend servers are over capacity. Try again later.'
489-
}
490467

491468
ANDROID_APPS_RESOURCE_NAME = 'androidApps'
492469
ANDROID_APP_IDENTIFIER_NAME = 'packageName'
493-
ANDROID_APP_IDENTIFIER_LABEL = 'Package name'
494470
IOS_APPS_RESOURCE_NAME = 'iosApps'
495471
IOS_APP_IDENTIFIER_NAME = 'bundleId'
496-
IOS_APP_IDENTIFIER_LABEL = 'Bundle ID'
497472

498473
def __init__(self, app):
499474
project_id = app.project_id
@@ -528,7 +503,7 @@ def _get_app_metadata(self, platform_resource_name, identifier_name, metadata_cl
528503
"""Retrieves detailed information about an Android or iOS app."""
529504
_check_is_nonempty_string(app_id, 'app_id')
530505
path = '/v1beta1/projects/-/{0}/{1}'.format(platform_resource_name, app_id)
531-
response = self._make_request('get', path, app_id, 'App ID')
506+
response = self._make_request('get', path)
532507
return metadata_class(
533508
response[identifier_name],
534509
name=response['name'],
@@ -553,7 +528,7 @@ def _set_display_name(self, app_id, new_display_name, platform_resource_name):
553528
path = '/v1beta1/projects/-/{0}/{1}?updateMask=displayName'.format(
554529
platform_resource_name, app_id)
555530
request_body = {'displayName': new_display_name}
556-
self._make_request('patch', path, app_id, 'App ID', json=request_body)
531+
self._make_request('patch', path, json=request_body)
557532

558533
def list_android_apps(self):
559534
return self._list_apps(
@@ -571,7 +546,7 @@ def _list_apps(self, platform_resource_name, app_class):
571546
self._project_id,
572547
platform_resource_name,
573548
_ProjectManagementService.MAXIMUM_LIST_APPS_PAGE_SIZE)
574-
response = self._make_request('get', path, self._project_id, 'Project ID')
549+
response = self._make_request('get', path)
575550
apps_list = []
576551
while True:
577552
apps = response.get('apps')
@@ -587,14 +562,13 @@ def _list_apps(self, platform_resource_name, app_class):
587562
platform_resource_name,
588563
next_page_token,
589564
_ProjectManagementService.MAXIMUM_LIST_APPS_PAGE_SIZE)
590-
response = self._make_request('get', path, self._project_id, 'Project ID')
565+
response = self._make_request('get', path)
591566
return apps_list
592567

593568
def create_android_app(self, package_name, display_name=None):
594569
return self._create_app(
595570
platform_resource_name=_ProjectManagementService.ANDROID_APPS_RESOURCE_NAME,
596571
identifier_name=_ProjectManagementService.ANDROID_APP_IDENTIFIER_NAME,
597-
identifier_label=_ProjectManagementService.ANDROID_APP_IDENTIFIER_LABEL,
598572
identifier=package_name,
599573
display_name=display_name,
600574
app_class=AndroidApp)
@@ -603,7 +577,6 @@ def create_ios_app(self, bundle_id, display_name=None):
603577
return self._create_app(
604578
platform_resource_name=_ProjectManagementService.IOS_APPS_RESOURCE_NAME,
605579
identifier_name=_ProjectManagementService.IOS_APP_IDENTIFIER_NAME,
606-
identifier_label=_ProjectManagementService.IOS_APP_IDENTIFIER_LABEL,
607580
identifier=bundle_id,
608581
display_name=display_name,
609582
app_class=IosApp)
@@ -612,7 +585,6 @@ def _create_app(
612585
self,
613586
platform_resource_name,
614587
identifier_name,
615-
identifier_label,
616588
identifier,
617589
display_name,
618590
app_class):
@@ -622,15 +594,10 @@ def _create_app(
622594
request_body = {identifier_name: identifier}
623595
if display_name:
624596
request_body['displayName'] = display_name
625-
response = self._make_request('post', path, identifier, identifier_label, json=request_body)
597+
response = self._make_request('post', path, json=request_body)
626598
operation_name = response['name']
627-
try:
628-
poll_response = self._poll_app_creation(operation_name)
629-
return app_class(app_id=poll_response['appId'], service=self)
630-
except _PollingError as error:
631-
raise ApiCallError(
632-
_ProjectManagementService._extract_message(operation_name, 'Operation name', error),
633-
error)
599+
poll_response = self._poll_app_creation(operation_name)
600+
return app_class(app_id=poll_response['appId'], service=self)
634601

635602
def _poll_app_creation(self, operation_name):
636603
"""Polls the Long-Running Operation repeatedly until it is done with exponential backoff."""
@@ -640,16 +607,17 @@ def _poll_app_creation(self, operation_name):
640607
wait_time_seconds = delay_factor * _ProjectManagementService.POLL_BASE_WAIT_TIME_SECONDS
641608
time.sleep(wait_time_seconds)
642609
path = '/v1/{0}'.format(operation_name)
643-
poll_response = self._make_request('get', path, operation_name, 'Operation name')
610+
poll_response, http_response = self._body_and_response('get', path)
644611
done = poll_response.get('done')
645612
if done:
646613
response = poll_response.get('response')
647614
if response:
648615
return response
649616
else:
650-
raise _PollingError(
651-
'Polling finished, but the operation terminated in an error.')
652-
raise _PollingError('Polling deadline exceeded.')
617+
raise exceptions.UnknownError(
618+
'Polling finished, but the operation terminated in an error.',
619+
http_response=http_response)
620+
raise exceptions.DeadlineExceededError('Polling deadline exceeded.')
653621

654622
def get_android_app_config(self, app_id):
655623
return self._get_app_config(
@@ -662,14 +630,14 @@ def get_ios_app_config(self, app_id):
662630

663631
def _get_app_config(self, platform_resource_name, app_id):
664632
path = '/v1beta1/projects/-/{0}/{1}/config'.format(platform_resource_name, app_id)
665-
response = self._make_request('get', path, app_id, 'App ID')
633+
response = self._make_request('get', path)
666634
# In Python 2.7, the base64 module works with strings, while in Python 3, it works with
667635
# bytes objects. This line works in both versions.
668636
return base64.standard_b64decode(response['configFileContents']).decode(encoding='utf-8')
669637

670638
def get_sha_certificates(self, app_id):
671639
path = '/v1beta1/projects/-/androidApps/{0}/sha'.format(app_id)
672-
response = self._make_request('get', path, app_id, 'App ID')
640+
response = self._make_request('get', path)
673641
cert_list = response.get('certificates') or []
674642
return [ShaCertificate(sha_hash=cert['shaHash'], name=cert['name']) for cert in cert_list]
675643

@@ -678,28 +646,20 @@ def add_sha_certificate(self, app_id, certificate_to_add):
678646
sha_hash = _check_not_none(certificate_to_add, 'certificate_to_add').sha_hash
679647
cert_type = certificate_to_add.cert_type
680648
request_body = {'shaHash': sha_hash, 'certType': cert_type}
681-
self._make_request('post', path, app_id, 'App ID', json=request_body)
649+
self._make_request('post', path, json=request_body)
682650

683651
def delete_sha_certificate(self, certificate_to_delete):
684652
name = _check_not_none(certificate_to_delete, 'certificate_to_delete').name
685653
path = '/v1beta1/{0}'.format(name)
686-
self._make_request('delete', path, name, 'SHA ID')
654+
self._make_request('delete', path)
655+
656+
def _make_request(self, method, url, json=None):
657+
body, _ = self._body_and_response(method, url, json)
658+
return body
687659

688-
def _make_request(self, method, url, resource_identifier, resource_identifier_label, json=None):
660+
def _body_and_response(self, method, url, json=None):
689661
try:
690-
return self._client.body(method=method, url=url, json=json, timeout=self._timeout)
662+
return self._client.body_and_response(
663+
method=method, url=url, json=json, timeout=self._timeout)
691664
except requests.exceptions.RequestException as error:
692-
raise ApiCallError(
693-
_ProjectManagementService._extract_message(
694-
resource_identifier, resource_identifier_label, error),
695-
error)
696-
697-
@staticmethod
698-
def _extract_message(identifier, identifier_label, error):
699-
if not isinstance(error, requests.exceptions.RequestException) or error.response is None:
700-
return '{0} "{1}": {2}'.format(identifier_label, identifier, str(error))
701-
status = error.response.status_code
702-
message = _ProjectManagementService.ERROR_CODES.get(status)
703-
if message:
704-
return '{0} "{1}": {2}'.format(identifier_label, identifier, message)
705-
return '{0} "{1}": Error {2}.'.format(identifier_label, identifier, status)
665+
raise _utils.handle_platform_error_from_requests(error)

integration/test_project_management.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import pytest
2222

23+
from firebase_admin import exceptions
2324
from firebase_admin import project_management
2425

2526

@@ -64,11 +65,12 @@ def ios_app(default_app):
6465
def test_create_android_app_already_exists(android_app):
6566
del android_app
6667

67-
with pytest.raises(project_management.ApiCallError) as excinfo:
68+
with pytest.raises(exceptions.AlreadyExistsError) as excinfo:
6869
project_management.create_android_app(
6970
package_name=TEST_APP_PACKAGE_NAME, display_name=TEST_APP_DISPLAY_NAME_PREFIX)
70-
assert 'The resource already exists' in str(excinfo.value)
71-
assert excinfo.value.detail is not None
71+
assert 'Requested entity already exists' in str(excinfo.value)
72+
assert excinfo.value.cause is not None
73+
assert excinfo.value.http_response is not None
7274

7375

7476
def test_android_set_display_name_and_get_metadata(android_app, project_id):
@@ -133,10 +135,11 @@ def test_android_sha_certificates(android_app):
133135
assert cert.name
134136

135137
# Adding the same cert twice should cause an already-exists error.
136-
with pytest.raises(project_management.ApiCallError) as excinfo:
138+
with pytest.raises(exceptions.AlreadyExistsError) as excinfo:
137139
android_app.add_sha_certificate(project_management.ShaCertificate(SHA_256_HASH_2))
138-
assert 'The resource already exists' in str(excinfo.value)
139-
assert excinfo.value.detail is not None
140+
assert 'Requested entity already exists' in str(excinfo.value)
141+
assert excinfo.value.cause is not None
142+
assert excinfo.value.http_response is not None
140143

141144
# Delete all certs and assert that they have all been deleted successfully.
142145
for cert in cert_list:
@@ -145,20 +148,22 @@ def test_android_sha_certificates(android_app):
145148
assert android_app.get_sha_certificates() == []
146149

147150
# Deleting a nonexistent cert should cause a not-found error.
148-
with pytest.raises(project_management.ApiCallError) as excinfo:
151+
with pytest.raises(exceptions.NotFoundError) as excinfo:
149152
android_app.delete_sha_certificate(cert_list[0])
150-
assert 'Failed to find the resource' in str(excinfo.value)
151-
assert excinfo.value.detail is not None
153+
assert 'Requested entity was not found' in str(excinfo.value)
154+
assert excinfo.value.cause is not None
155+
assert excinfo.value.http_response is not None
152156

153157

154158
def test_create_ios_app_already_exists(ios_app):
155159
del ios_app
156160

157-
with pytest.raises(project_management.ApiCallError) as excinfo:
161+
with pytest.raises(exceptions.AlreadyExistsError) as excinfo:
158162
project_management.create_ios_app(
159163
bundle_id=TEST_APP_BUNDLE_ID, display_name=TEST_APP_DISPLAY_NAME_PREFIX)
160-
assert 'The resource already exists' in str(excinfo.value)
161-
assert excinfo.value.detail is not None
164+
assert 'Requested entity already exists' in str(excinfo.value)
165+
assert excinfo.value.cause is not None
166+
assert excinfo.value.http_response is not None
162167

163168

164169
def test_ios_set_display_name_and_get_metadata(ios_app, project_id):

0 commit comments

Comments
 (0)