Skip to content

Commit 508a39b

Browse files
authored
Google Cloud Storage API [WIP] (#44)
* Delegating token generation to the google.auth.credentials library * Provided a default implementation for get_access_token() * Updated API doc * Experimental GCS integration * Adding GCS dependency * Temp fix for linter false positives * Refactored storage API and added unit tests * Raising a ValueError for non-existing buckets * Custom handling import error * Removing the checks for non-existing buckets * Test case for valid bucket config * Updated setup script. Added GCS dependency.
1 parent d86b8bb commit 508a39b

File tree

8 files changed

+204
-1
lines changed

8 files changed

+204
-1
lines changed

firebase_admin/credentials.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
_request = requests.Request()
2727
_scopes = [
28+
'https://www.googleapis.com/auth/devstorage.read_write',
2829
'https://www.googleapis.com/auth/firebase',
2930
'https://www.googleapis.com/auth/userinfo.email'
3031
]

firebase_admin/storage.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Copyright 2017 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Firebase Cloud Storage module.
16+
17+
This module contains utilities for accessing Google Cloud Storage buckets associated with
18+
Firebase apps. This requires installing the google-cloud-storage Python module separately.
19+
"""
20+
21+
# pylint: disable=import-error,no-name-in-module
22+
try:
23+
from google.cloud import storage
24+
except ImportError:
25+
raise ImportError('Failed to import the Cloud Storage library for Python. Make sure '
26+
'to install the "google-cloud-storage" module.')
27+
28+
import six
29+
30+
from firebase_admin import utils
31+
32+
33+
_STORAGE_ATTRIBUTE = '_storage'
34+
35+
def bucket(name=None, app=None):
36+
"""Returns a handle to a Google Cloud Storage bucket.
37+
38+
If the name argument is not provided, uses the 'storageBucket' option specified when
39+
initializing the App. If that is also not available raises an error. This function
40+
does not make any RPC calls.
41+
42+
Args:
43+
name: Name of a cloud storage bucket (optional).
44+
app: An App instance (optional).
45+
46+
Returns:
47+
google.cloud.storage.Bucket: A handle to the specified bucket.
48+
49+
Raises:
50+
ValueError: If a bucket name is not specified either via options or method arguments,
51+
or if the specified bucket name is not a valid string.
52+
"""
53+
client = utils.get_app_service(app, _STORAGE_ATTRIBUTE, _StorageClient.from_app)
54+
return client.bucket(name)
55+
56+
57+
class _StorageClient(object):
58+
"""Holds a Google Cloud Storage client instance."""
59+
60+
def __init__(self, credentials, project, default_bucket):
61+
self._client = storage.Client(credentials=credentials, project=project)
62+
self._default_bucket = default_bucket
63+
64+
@classmethod
65+
def from_app(cls, app):
66+
credentials = app.credential.get_credential()
67+
# Specifying project ID is not required, but providing it when available
68+
# significantly speeds up the initialization of the storage client.
69+
try:
70+
project = app.credential.project_id
71+
except AttributeError:
72+
project = None
73+
default_bucket = app.options.get('storageBucket')
74+
return _StorageClient(credentials, project, default_bucket)
75+
76+
def bucket(self, name=None):
77+
"""Returns a handle to the specified Cloud Storage Bucket."""
78+
bucket_name = name if name is not None else self._default_bucket
79+
if bucket_name is None:
80+
raise ValueError(
81+
'Storage bucket name not specified. Specify the bucket name via the '
82+
'"storageBucket" option when initializing the App, or specify the bucket '
83+
'name explicitly when calling the storage.bucket() function.')
84+
elif not bucket_name or not isinstance(bucket_name, six.string_types):
85+
raise ValueError(
86+
'Invalid storage bucket name: "{0}". Bucket name must be a non-empty '
87+
'string.'.format(bucket_name))
88+
return self._client.bucket(bucket_name)

integration/conftest.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ def integration_conf(request):
4242
raise ValueError('Failed to determine project ID from service account certificate.')
4343
return credentials.Certificate(cert_path), project_id
4444

45+
@pytest.fixture(scope='session')
46+
def project_id(request):
47+
_, project_id = integration_conf(request)
48+
return project_id
49+
4550
@pytest.fixture(autouse=True, scope='session')
4651
def default_app(request):
4752
"""Initializes the default Firebase App instance used for all integration tests.
@@ -51,7 +56,10 @@ def default_app(request):
5156
test cases having to call it explicitly.
5257
"""
5358
cred, project_id = integration_conf(request)
54-
ops = {'databaseURL' : 'https://{0}.firebaseio.com'.format(project_id)}
59+
ops = {
60+
'databaseURL' : 'https://{0}.firebaseio.com'.format(project_id),
61+
'storageBucket' : '{0}.appspot.com'.format(project_id)
62+
}
5563
return firebase_admin.initialize_app(cred, ops)
5664

5765
@pytest.fixture(scope='session')

integration/test_storage.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2017 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Integration tests for firebase_admin.storage module."""
16+
import time
17+
18+
from firebase_admin import storage
19+
20+
21+
def test_default_bucket(project_id):
22+
bucket = storage.bucket()
23+
_verify_bucket(bucket, '{0}.appspot.com'.format(project_id))
24+
25+
def test_custom_bucket(project_id):
26+
bucket_name = '{0}.appspot.com'.format(project_id)
27+
bucket = storage.bucket(bucket_name)
28+
_verify_bucket(bucket, bucket_name)
29+
30+
def test_non_existing_bucket():
31+
bucket = storage.bucket('non.existing')
32+
assert bucket.exists() is False
33+
34+
def _verify_bucket(bucket, expected_name):
35+
assert bucket.name == expected_name
36+
file_name = 'data_{0}.txt'.format(int(time.time()))
37+
blob = bucket.blob(file_name)
38+
blob.upload_from_string('Hello World')
39+
40+
blob = bucket.get_blob(file_name)
41+
assert blob.download_as_string() == 'Hello World'
42+
43+
bucket.delete_blob(file_name)
44+
assert not bucket.get_blob(file_name)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ pytest-cov >= 2.4.0
44
tox >= 2.6.0
55

66
google-auth >= 1.0.0
7+
google-cloud-storage >= 1.2.0
78
requests >= 2.13.0
89
six >= 1.6.1

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
'to integrate Firebase into their services and applications.')
3434
install_requires = [
3535
'google-auth>=1.0.0',
36+
'google-cloud-storage>=1.2.0',
3637
'requests>=2.13.0',
3738
'six>=1.6.1'
3839
]

tests/test_app.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import firebase_admin
2121
from firebase_admin import credentials
22+
from firebase_admin import utils
2223
from tests import testutils
2324

2425

@@ -68,6 +69,11 @@ def get(self):
6869
return None
6970

7071

72+
class AppService(object):
73+
def __init__(self, app):
74+
self._app = app
75+
76+
7177
@pytest.fixture(params=[Cert(), RefreshToken(), ExplicitAppDefault(), ImplicitAppDefault()],
7278
ids=['cert', 'refreshtoken', 'explicit-appdefault', 'implicit-appdefault'])
7379
def app_credential(request):
@@ -159,3 +165,12 @@ def test_app_delete(self, init_app):
159165
firebase_admin.get_app(init_app.name)
160166
with pytest.raises(ValueError):
161167
firebase_admin.delete_app(init_app)
168+
169+
def test_app_services(self, init_app):
170+
service = utils.get_app_service(init_app, 'test.service', AppService)
171+
assert isinstance(service, AppService)
172+
service2 = utils.get_app_service(init_app, 'test.service', AppService)
173+
assert service is service2
174+
firebase_admin.delete_app(init_app)
175+
with pytest.raises(ValueError):
176+
utils.get_app_service(init_app, 'test.service', AppService)

tests/test_storage.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright 2017 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Tests for firebase_admin.storage."""
16+
17+
import pytest
18+
19+
import firebase_admin
20+
from firebase_admin import credentials
21+
from firebase_admin import storage
22+
from tests import testutils
23+
24+
25+
def setup_module():
26+
cred = credentials.Certificate(testutils.resource_filename('service_account.json'))
27+
firebase_admin.initialize_app(cred)
28+
29+
def teardown_module():
30+
testutils.cleanup_apps()
31+
32+
def test_invalid_config():
33+
with pytest.raises(ValueError):
34+
storage.bucket()
35+
36+
@pytest.mark.parametrize('name', [None, '', 0, 1, True, False, list(), tuple(), dict()])
37+
def test_invalid_name(name):
38+
with pytest.raises(ValueError):
39+
storage.bucket(name)
40+
41+
def test_valid_name():
42+
# Should not make RPC calls.
43+
bucket = storage.bucket('foo')
44+
assert bucket is not None
45+
assert bucket.name == 'foo'

0 commit comments

Comments
 (0)