Skip to content
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
86 changes: 85 additions & 1 deletion gcloud/bigquery/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,29 @@
from gcloud.bigquery.table import Table


class AccessGrant(object):
"""Represent grant of an access role to an entity.

:type role: string (one of 'OWNER', 'WRITER', 'READER').
:param role: role granted to the entity.

:type entity_type: string (one of 'specialGroup', 'groupByEmail', or
'userByEmail')
:param entity_type: type of entity being granted the role.

:type entity_id: string
:param entity_id: ID of entity being granted the role.
"""
def __init__(self, role, entity_type, entity_id):
self.role = role
self.entity_type = entity_type
self.entity_id = entity_id

def __repr__(self):
return '<AccessGrant: role=%s, %s=%s>' % (
self.role, self.entity_type, self.entity_id)


class Dataset(object):
"""Datasets are containers for tables.

Expand All @@ -32,12 +55,16 @@ class Dataset(object):
:type client: :class:`gcloud.bigquery.client.Client`
:param client: A client which holds credentials and project configuration
for the dataset (which requires a project).

:type access_grants: list of :class:`AccessGrant`
:param access_grants: roles granted to entities for this dataset
"""

def __init__(self, name, client):
def __init__(self, name, client, access_grants=()):
self.name = name
self._client = client
self._properties = {}
self.access_grants = access_grants

This comment was marked as spam.


@property
def project(self):
Expand All @@ -57,6 +84,29 @@ def path(self):
"""
return '/projects/%s/datasets/%s' % (self.project, self.name)

@property
def access_grants(self):
"""Dataset's access grants.

:rtype: list of :class:`AccessGrant`
:returns: roles granted to entities for this dataset
"""
return list(self._access_grants)

@access_grants.setter
def access_grants(self, value):
"""Update dataset's access grants

:type value: list of :class:`AccessGrant`
:param value: roles granted to entities for this dataset

:raises: TypeError if 'value' is not a sequence, or ValueError if
any item in the sequence is not an AccessGrant
"""
if not all(isinstance(field, AccessGrant) for field in value):
raise ValueError('Values must be AccessGrant instances')
self._access_grants = tuple(value)

@property
def created(self):
"""Datetime at which the dataset was created.
Expand Down Expand Up @@ -227,6 +277,27 @@ def _require_client(self, client):
client = self._client
return client

def _parse_access_grants(self, access):
"""Parse a resource fragment into a set of access grants.

:type access: list of mappings
:param access: each mapping represents a single access grant

:rtype: list of :class:`AccessGrant`
:returns: a list of parsed grants
"""
result = []
for grant in access:
grant = grant.copy()
role = grant.pop('role')
# Hypothetical case: we don't know that the back-end will ever
# return such structures, but they are logical. See:
# https://github.com/GoogleCloudPlatform/gcloud-python/pull/1046#discussion_r36687769
for entity_type, entity_id in sorted(grant.items()):
result.append(
AccessGrant(role, entity_type, entity_id))
return result

def _set_properties(self, api_response):
"""Update properties from resource in body of ``api_response``

Expand All @@ -235,12 +306,22 @@ def _set_properties(self, api_response):
"""
self._properties.clear()
cleaned = api_response.copy()
access = cleaned.pop('access', ())
self.access_grants = self._parse_access_grants(access)
if 'creationTime' in cleaned:
cleaned['creationTime'] = float(cleaned['creationTime'])
if 'lastModifiedTime' in cleaned:
cleaned['lastModifiedTime'] = float(cleaned['lastModifiedTime'])
self._properties.update(cleaned)

def _build_access_resource(self):

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

"""Generate a resource fragment for dataset's access grants."""
result = []
for grant in self.access_grants:
info = {'role': grant.role, grant.entity_type: grant.entity_id}
result.append(info)
return result

def _build_resource(self):
"""Generate a resource for ``create`` or ``update``."""
resource = {
Expand All @@ -260,6 +341,9 @@ def _build_resource(self):
if self.location is not None:
resource['location'] = self.location

if len(self.access_grants) > 0:
resource['access'] = self._build_access_resource()

return resource

def create(self, client=None):
Expand Down
123 changes: 122 additions & 1 deletion gcloud/bigquery/test_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@
import unittest2


class TestAccessGrant(unittest2.TestCase):

def _getTargetClass(self):
from gcloud.bigquery.dataset import AccessGrant
return AccessGrant

def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)

def test_ctor_defaults(self):
grant = self._makeOne('OWNER', 'userByEmail', '[email protected]')
self.assertEqual(grant.role, 'OWNER')
self.assertEqual(grant.entity_type, 'userByEmail')
self.assertEqual(grant.entity_id, '[email protected]')


class TestDataset(unittest2.TestCase):
PROJECT = 'project'
DS_NAME = 'dataset-name'
Expand All @@ -39,6 +55,8 @@ def _setUpConstants(self):

def _makeResource(self):
self._setUpConstants()
USER_EMAIL = '[email protected]'
GROUP_EMAIL = '[email protected]'
return {
'creationTime': self.WHEN_TS * 1000,
'datasetReference':
Expand All @@ -48,10 +66,32 @@ def _makeResource(self):
'lastModifiedTime': self.WHEN_TS * 1000,
'location': 'US',
'selfLink': self.RESOURCE_URL,
'access': [
{'role': 'OWNER', 'userByEmail': USER_EMAIL},
{'role': 'OWNER', 'groupByEmail': GROUP_EMAIL},
{'role': 'WRITER', 'specialGroup': 'projectWriters'},
{'role': 'READER', 'specialGroup': 'projectReaders'}],
}

def _verifyResourceProperties(self, dataset, resource):
def _verifyAccessGrants(self, access_grants, resource):
r_grants = []
for r_grant in resource['access']:
role = r_grant.pop('role')
for entity_type, entity_id in sorted(r_grant.items()):
r_grants.append({'role': role,
'entity_type': entity_type,
'entity_id': entity_id})

self.assertEqual(len(access_grants), len(r_grants))
for a_grant, r_grant in zip(access_grants, r_grants):
self.assertEqual(a_grant.role, r_grant['role'])
self.assertEqual(a_grant.entity_type, r_grant['entity_type'])
self.assertEqual(a_grant.entity_id, r_grant['entity_id'])

def _verifyReadonlyResourceProperties(self, dataset, resource):

self.assertEqual(dataset.dataset_id, self.DS_ID)

if 'creationTime' in resource:
self.assertEqual(dataset.created, self.WHEN)
else:
Expand All @@ -69,12 +109,21 @@ def _verifyResourceProperties(self, dataset, resource):
else:
self.assertEqual(dataset.self_link, None)

def _verifyResourceProperties(self, dataset, resource):

self._verifyReadonlyResourceProperties(dataset, resource)

self.assertEqual(dataset.default_table_expiration_ms,
resource.get('defaultTableExpirationMs'))
self.assertEqual(dataset.description, resource.get('description'))
self.assertEqual(dataset.friendly_name, resource.get('friendlyName'))
self.assertEqual(dataset.location, resource.get('location'))

if 'access' in resource:
self._verifyAccessGrants(dataset.access_grants, resource)
else:
self.assertEqual(dataset.access_grants, [])

def test_ctor(self):
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client)
Expand All @@ -84,6 +133,7 @@ def test_ctor(self):
self.assertEqual(
dataset.path,
'/projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME))
self.assertEqual(dataset.access_grants, [])

self.assertEqual(dataset.created, None)
self.assertEqual(dataset.dataset_id, None)
Expand All @@ -96,6 +146,29 @@ def test_ctor(self):
self.assertEqual(dataset.friendly_name, None)
self.assertEqual(dataset.location, None)

def test_access_roles_setter_non_list(self):
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client)
with self.assertRaises(TypeError):
dataset.access_grants = object()

def test_access_roles_setter_invalid_field(self):
from gcloud.bigquery.dataset import AccessGrant
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client)
phred = AccessGrant('OWNER', 'userByEmail', '[email protected]')
with self.assertRaises(ValueError):
dataset.access_grants = [phred, object()]

def test_access_roles_setter(self):
from gcloud.bigquery.dataset import AccessGrant
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client)
phred = AccessGrant('OWNER', 'userByEmail', '[email protected]')
bharney = AccessGrant('OWNER', 'userByEmail', '[email protected]')
dataset.access_grants = [phred, bharney]
self.assertEqual(dataset.access_grants, [phred, bharney])

def test_default_table_expiration_ms_setter_bad_value(self):
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client)
Expand Down Expand Up @@ -175,6 +248,41 @@ def test_from_api_repr_w_properties(self):
self.assertTrue(dataset._client is client)
self._verifyResourceProperties(dataset, RESOURCE)

def test__parse_access_grants_w_unknown_entity_type(self):
USER_EMAIL = '[email protected]'
GROUP_EMAIL = '[email protected]'
RESOURCE = {
'access': [
{'role': 'OWNER', 'userByEmail': USER_EMAIL},
{'role': 'WRITER', 'groupByEmail': GROUP_EMAIL},
{'role': 'READER', 'specialGroup': 'projectReaders'},
{'role': 'READER', 'unknown': 'UNKNOWN'}]
}
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client=client)
grants = dataset._parse_access_grants(RESOURCE['access'])
self._verifyAccessGrants(grants, RESOURCE)

def test__parse_access_grants_w_multiple_entity_types(self):
# Hypothetical case: we don't know that the back-end will ever
# return such structures, but they are logical. See:
# https://github.com/GoogleCloudPlatform/gcloud-python/pull/1046#discussion_r36687769
USER_EMAIL = '[email protected]'
OTHER_EMAIL = '[email protected]'
GROUP_EMAIL = '[email protected]'
RESOURCE = {
'access': [
{'role': 'OWNER', 'userByEmail': USER_EMAIL},
{'role': 'WRITER', 'groupByEmail': GROUP_EMAIL},
{'role': 'READER',
'specialGroup': 'projectReaders',
'userByEmail': OTHER_EMAIL}]
}
client = _Client(self.PROJECT)
dataset = self._makeOne(self.DS_NAME, client=client)
grants = dataset._parse_access_grants(RESOURCE['access'])
self._verifyAccessGrants(grants, RESOURCE)

def test_create_w_bound_client(self):
PATH = 'projects/%s/datasets' % self.PROJECT
RESOURCE = self._makeResource()
Expand All @@ -196,7 +304,10 @@ def test_create_w_bound_client(self):
self._verifyResourceProperties(dataset, RESOURCE)

def test_create_w_alternate_client(self):
from gcloud.bigquery.dataset import AccessGrant
PATH = 'projects/%s/datasets' % self.PROJECT
USER_EMAIL = '[email protected]'
GROUP_EMAIL = '[email protected]'
DESCRIPTION = 'DESCRIPTION'
TITLE = 'TITLE'
RESOURCE = self._makeResource()
Expand All @@ -209,6 +320,11 @@ def test_create_w_alternate_client(self):
dataset = self._makeOne(self.DS_NAME, client=CLIENT1)
dataset.friendly_name = TITLE
dataset.description = DESCRIPTION
dataset.access_grants = [
AccessGrant('OWNER', 'userByEmail', USER_EMAIL),
AccessGrant('OWNER', 'groupByEmail', GROUP_EMAIL),
AccessGrant('READER', 'specialGroup', 'projectReaders'),
AccessGrant('WRITER', 'specialGroup', 'projectWriters')]

dataset.create(client=CLIENT2)

Expand All @@ -222,6 +338,11 @@ def test_create_w_alternate_client(self):
{'projectId': self.PROJECT, 'datasetId': self.DS_NAME},
'description': DESCRIPTION,
'friendlyName': TITLE,
'access': [
{'role': 'OWNER', 'userByEmail': USER_EMAIL},
{'role': 'OWNER', 'groupByEmail': GROUP_EMAIL},
{'role': 'READER', 'specialGroup': 'projectReaders'},
{'role': 'WRITER', 'specialGroup': 'projectWriters'}],
}
self.assertEqual(req['data'], SENT)
self._verifyResourceProperties(dataset, RESOURCE)
Expand Down