Skip to content

Commit c098cd0

Browse files
authored
feat: support authorized dataset entity (#1075)
* feat: support authorized dataset entity * cleanup * add test and cache the resource from from_api_repr in a _properties value * lint * update samples to use enums * update to_api_repr and add tests * refactor
1 parent 44221f4 commit c098cd0

File tree

5 files changed

+92
-43
lines changed

5 files changed

+92
-43
lines changed

google/cloud/bigquery/dataset.py

+35-38
Original file line numberDiff line numberDiff line change
@@ -77,28 +77,29 @@ def _get_routine_reference(self, routine_id):
7777
class AccessEntry(object):
7878
"""Represents grant of an access role to an entity.
7979
80-
An entry must have exactly one of the allowed :attr:`ENTITY_TYPES`. If
81-
anything but ``view`` or ``routine`` are set, a ``role`` is also required.
82-
``role`` is omitted for ``view`` and ``routine``, because they are always
83-
read-only.
80+
An entry must have exactly one of the allowed
81+
:class:`google.cloud.bigquery.enums.EntityTypes`. If anything but ``view``, ``routine``,
82+
or ``dataset`` are set, a ``role`` is also required. ``role`` is omitted for ``view``,
83+
``routine``, ``dataset``, because they are always read-only.
8484
8585
See https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets.
8686
8787
Args:
8888
role (str):
8989
Role granted to the entity. The following string values are
9090
supported: `'READER'`, `'WRITER'`, `'OWNER'`. It may also be
91-
:data:`None` if the ``entity_type`` is ``view`` or ``routine``.
91+
:data:`None` if the ``entity_type`` is ``view``, ``routine``, or ``dataset``.
9292
9393
entity_type (str):
94-
Type of entity being granted the role. One of :attr:`ENTITY_TYPES`.
94+
Type of entity being granted the role. See
95+
:class:`google.cloud.bigquery.enums.EntityTypes` for supported types.
9596
9697
entity_id (Union[str, Dict[str, str]]):
97-
If the ``entity_type`` is not 'view' or 'routine', the ``entity_id``
98-
is the ``str`` ID of the entity being granted the role. If the
99-
``entity_type`` is 'view' or 'routine', the ``entity_id`` is a ``dict``
100-
representing the view or routine from a different dataset to grant
101-
access to in the following format for views::
98+
If the ``entity_type`` is not 'view', 'routine', or 'dataset', the
99+
``entity_id`` is the ``str`` ID of the entity being granted the role. If
100+
the ``entity_type`` is 'view' or 'routine', the ``entity_id`` is a ``dict``
101+
representing the view or routine from a different dataset to grant access
102+
to in the following format for views::
102103
103104
{
104105
'projectId': string,
@@ -114,11 +115,22 @@ class AccessEntry(object):
114115
'routineId': string
115116
}
116117
118+
If the ``entity_type`` is 'dataset', the ``entity_id`` is a ``dict`` that includes
119+
a 'dataset' field with a ``dict`` representing the dataset and a 'target_types'
120+
field with a ``str`` value of the dataset's resource type::
121+
122+
{
123+
'dataset': {
124+
'projectId': string,
125+
'datasetId': string,
126+
},
127+
'target_types: 'VIEWS'
128+
}
129+
117130
Raises:
118131
ValueError:
119-
If the ``entity_type`` is not among :attr:`ENTITY_TYPES`, or if a
120-
``view`` or a ``routine`` has ``role`` set, or a non ``view`` and
121-
non ``routine`` **does not** have a ``role`` set.
132+
If a ``view``, ``routine``, or ``dataset`` has ``role`` set, or a non ``view``,
133+
non ``routine``, and non ``dataset`` **does not** have a ``role`` set.
122134
123135
Examples:
124136
>>> entry = AccessEntry('OWNER', 'userByEmail', '[email protected]')
@@ -131,27 +143,9 @@ class AccessEntry(object):
131143
>>> entry = AccessEntry(None, 'view', view)
132144
"""
133145

134-
ENTITY_TYPES = frozenset(
135-
[
136-
"userByEmail",
137-
"groupByEmail",
138-
"domain",
139-
"specialGroup",
140-
"view",
141-
"iamMember",
142-
"routine",
143-
]
144-
)
145-
"""Allowed entity types."""
146-
147-
def __init__(self, role, entity_type, entity_id):
148-
if entity_type not in self.ENTITY_TYPES:
149-
message = "Entity type %r not among: %s" % (
150-
entity_type,
151-
", ".join(self.ENTITY_TYPES),
152-
)
153-
raise ValueError(message)
154-
if entity_type in ("view", "routine"):
146+
def __init__(self, role=None, entity_type=None, entity_id=None):
147+
self._properties = {}
148+
if entity_type in ("view", "routine", "dataset"):
155149
if role is not None:
156150
raise ValueError(
157151
"Role must be None for a %r. Received "
@@ -162,7 +156,6 @@ def __init__(self, role, entity_type, entity_id):
162156
raise ValueError(
163157
"Role must be set for entity " "type %r" % (entity_type,)
164158
)
165-
166159
self._role = role
167160
self._entity_type = entity_type
168161
self._entity_id = entity_id
@@ -214,7 +207,8 @@ def to_api_repr(self):
214207
Returns:
215208
Dict[str, object]: Access entry represented as an API resource
216209
"""
217-
resource = {self._entity_type: self._entity_id}
210+
resource = copy.deepcopy(self._properties)
211+
resource[self._entity_type] = self._entity_id
218212
if self._role is not None:
219213
resource["role"] = self._role
220214
return resource
@@ -241,7 +235,10 @@ def from_api_repr(cls, resource: dict) -> "AccessEntry":
241235
entity_type, entity_id = entry.popitem()
242236
if len(entry) != 0:
243237
raise ValueError("Entry has unexpected keys remaining.", entry)
244-
return cls(role, entity_type, entity_id)
238+
239+
config = cls(role, entity_type, entity_id)
240+
config._properties = copy.deepcopy(resource)
241+
return config
245242

246243

247244
class DatasetReference(object):

google/cloud/bigquery/enums.py

+13
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,19 @@ def _make_sql_scalars_enum():
232232
StandardSqlDataTypes = _make_sql_scalars_enum()
233233

234234

235+
class EntityTypes(str, enum.Enum):
236+
"""Enum of allowed entity type names in AccessEntry"""
237+
238+
USER_BY_EMAIL = "userByEmail"
239+
GROUP_BY_EMAIL = "groupByEmail"
240+
DOMAIN = "domain"
241+
DATASET = "dataset"
242+
SPECIAL_GROUP = "specialGroup"
243+
VIEW = "view"
244+
IAM_MEMBER = "iamMember"
245+
ROUTINE = "routine"
246+
247+
235248
# See also: https://cloud.google.com/bigquery/data-types#legacy_sql_data_types
236249
# and https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types
237250
class SqlTypeNames(str, enum.Enum):

samples/snippets/authorized_view_tutorial.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def run_authorized_view_tutorial(override_values={}):
2424
# Create a source dataset
2525
# [START bigquery_avt_create_source_dataset]
2626
from google.cloud import bigquery
27+
from google.cloud.bigquery.enums import EntityTypes
2728

2829
client = bigquery.Client()
2930
source_dataset_id = "github_source_data"
@@ -106,7 +107,7 @@ def run_authorized_view_tutorial(override_values={}):
106107
# analyst_group_email = '[email protected]'
107108
access_entries = shared_dataset.access_entries
108109
access_entries.append(
109-
bigquery.AccessEntry("READER", "groupByEmail", analyst_group_email)
110+
bigquery.AccessEntry("READER", EntityTypes.GROUP_BY_EMAIL, analyst_group_email)
110111
)
111112
shared_dataset.access_entries = access_entries
112113
shared_dataset = client.update_dataset(
@@ -118,7 +119,7 @@ def run_authorized_view_tutorial(override_values={}):
118119
# [START bigquery_avt_source_dataset_access]
119120
access_entries = source_dataset.access_entries
120121
access_entries.append(
121-
bigquery.AccessEntry(None, "view", view.reference.to_api_repr())
122+
bigquery.AccessEntry(None, EntityTypes.VIEW, view.reference.to_api_repr())
122123
)
123124
source_dataset.access_entries = access_entries
124125
source_dataset = client.update_dataset(

samples/snippets/update_dataset_access.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ def update_dataset_access(dataset_id: str, entity_id: str):
2727
# of the entity, such as a view's table reference.
2828
entity_id = "[email protected]"
2929

30+
from google.cloud.bigquery.enums import EntityTypes
31+
3032
# TODO(developer): Set entity_type to the type of entity you are granting access to.
3133
# Common types include:
3234
#
@@ -37,7 +39,7 @@ def update_dataset_access(dataset_id: str, entity_id: str):
3739
#
3840
# For a complete reference, see the REST API reference documentation:
3941
# https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets#Dataset.FIELDS.access
40-
entity_type = "groupByEmail"
42+
entity_type = EntityTypes.GROUP_BY_EMAIL
4143

4244
# TODO(developer): Set role to a one of the "Basic roles for datasets"
4345
# described here:

tests/unit/test_dataset.py

+38-2
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,28 @@ def test_to_api_repr_routine(self):
141141
exp_resource = {"routine": routine}
142142
self.assertEqual(resource, exp_resource)
143143

144+
def test_to_api_repr_dataset(self):
145+
dataset = {
146+
"dataset": {"projectId": "my-project", "datasetId": "my_dataset"},
147+
"target_types": "VIEWS",
148+
}
149+
entry = self._make_one(None, "dataset", dataset)
150+
resource = entry.to_api_repr()
151+
exp_resource = {"dataset": dataset}
152+
self.assertEqual(resource, exp_resource)
153+
154+
def test_to_api_w_incorrect_role(self):
155+
dataset = {
156+
"dataset": {
157+
"projectId": "my-project",
158+
"datasetId": "my_dataset",
159+
"tableId": "my_table",
160+
},
161+
"target_type": "VIEW",
162+
}
163+
with self.assertRaises(ValueError):
164+
self._make_one("READER", "dataset", dataset)
165+
144166
def test_from_api_repr(self):
145167
resource = {"role": "OWNER", "userByEmail": "[email protected]"}
146168
entry = self._get_target_class().from_api_repr(resource)
@@ -150,8 +172,22 @@ def test_from_api_repr(self):
150172

151173
def test_from_api_repr_w_unknown_entity_type(self):
152174
resource = {"role": "READER", "unknown": "UNKNOWN"}
153-
with self.assertRaises(ValueError):
154-
self._get_target_class().from_api_repr(resource)
175+
entry = self._get_target_class().from_api_repr(resource)
176+
self.assertEqual(entry.role, "READER")
177+
self.assertEqual(entry.entity_type, "unknown")
178+
self.assertEqual(entry.entity_id, "UNKNOWN")
179+
exp_resource = entry.to_api_repr()
180+
self.assertEqual(resource, exp_resource)
181+
182+
def test_to_api_repr_w_extra_properties(self):
183+
resource = {
184+
"role": "READER",
185+
"userByEmail": "[email protected]",
186+
}
187+
entry = self._get_target_class().from_api_repr(resource)
188+
entry._properties["specialGroup"] = resource["specialGroup"] = "projectReaders"
189+
exp_resource = entry.to_api_repr()
190+
self.assertEqual(resource, exp_resource)
155191

156192
def test_from_api_repr_entries_w_extra_keys(self):
157193
resource = {

0 commit comments

Comments
 (0)