Skip to content

Commit 7bbab3a

Browse files
feat(bigquery): create cloud-client samples (#13147)
* docs(bigquery): delete README.rst - Samples must be moved back to this directory. * feat(bigquery): add dependencies to cloud-client directory * feat(bigquery): create sample 'view dataset access policy' * feat(bigquery): fix license headers * feat(bigquery): improve view_dataset_access_policy sample - Change function name and region tag from '_policies' to '_policy', to match documentation - Add more explanation to how the Access policy is retreived as a list of Access Entries - Add a validation to check if 'access_entries' has elements * feat(bigquery): replace .format() with f-strings * feat(bigquery): apply feedback from PR review 2609806365 - Change typing imports as per PEP-585 and PEP-604 - Remove dependencies for Python < 3.9 - Refactor overriding values - Remove CLI entry point * feat(bigquery): apply feedback from PR review 2309806365 part 2 - Refactor creating and deleting datasets into a single fixture. - Refactor reading stdout/stderr to returning the dataset and asserting it has the right name. - Validate that the dataset was deleted successfully. * feat(bigquery): refactor sample view_dataset_access_policy - Assert over an Access policy, not the Dataset itself * feat(bigquery): add sample 'view_table_or_view_access_policy' * feat(bigquery): remove CLI entry point * feat(bigquery): WIP draft sample 'revoke_access_to_table_or_view' * feat(bigquery): apply feedback from PR review 2616192713 - Leave only one way to print access_entries, and refactor with a for to show all list elements. - Remove unnecessary validation for deleting dataset. - Remove unnecessary fixture for a second table when first one could be used to create the view. - Remove magic value for empty policy. * feat(bigquery): refactor printing dataset.access_entries - Works better when there is an empty AccessEntry list. * feat(bigquery): WIP draft sample 'grant_access_to_table_or_view' * feat(bigquery): start refactor to use conftest.py feature * feat(bigquery): WIP refactor samples to use conftest.py - Simplify use of fixtures. - Apply diverse feedback from the team. * feat(bigquery): remove EntityTypes dependency - Hardcoding the EntityType to simplify the sample. * feat(bitquery): refactor sample 'revoke_access_to_table_or_view' to unify it to Node.JS one - Allow to revoke access by role or by principal_id. * feat(bigquery): update conftest.py - Add entity_id - Remove unused comments * feat(bigquery): add test for grant_access_to_table_or_view * feat(bigquery): add test for revoke_access_to_table_or_view * feat(bigquery): fix samples for gran_access - Update principal_id sample with a valid identifier. * feat(bigquery): minor cleanup * feat(bigquery): add test for 'revoke_dataset_access' * feat(bigquery): add sample 'revoke_dataset_access' * feat(bigquery): refactor samples and tests for consistency - Remove unnecesary code. - Rewrite comments and documentation references. - Add __future__ annotations for Python 3.9 * feat(bigquery): improve comments on sample 'revoke_access_to_table_or_view' * feat(bigquery): improve comment in 'revoke_access_to_table_or_view' * feat(bigquery): cleanup and unify with Go samples - Rename 'bigquery_client' to 'client' to make it consistent across samples. - Add validation for empty bindings list in 'view_table_or_access_policy'. - Add '_id' suffix to placeholder.
1 parent 6abfe7c commit 7bbab3a

16 files changed

+797
-3
lines changed

bigquery/cloud-client/README.rst

Lines changed: 0 additions & 3 deletions
This file was deleted.

bigquery/cloud-client/conftest.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Copyright 2020 Google LLC
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+
from google.cloud import bigquery
16+
from google.cloud.bigquery.dataset import Dataset
17+
from google.cloud.bigquery.table import Table
18+
19+
import pytest
20+
import test_utils.prefixer
21+
22+
prefixer = test_utils.prefixer.Prefixer("python-docs-samples", "bigquery/cloud-client")
23+
24+
PREFIX = prefixer.create_prefix()
25+
ENTITY_ID = "cloud-developer-relations@google.com" # Group account
26+
DATASET_ID = f"{PREFIX}_cloud_client"
27+
TABLE_NAME = f"{PREFIX}_view_access_policies_table"
28+
VIEW_NAME = f"{PREFIX}_view_access_policies_view"
29+
30+
31+
@pytest.fixture(scope="module")
32+
def client() -> bigquery.Client:
33+
return bigquery.Client()
34+
35+
36+
@pytest.fixture(scope="module")
37+
def project_id(client: bigquery.Client) -> str:
38+
return client.project
39+
40+
41+
@pytest.fixture(scope="module")
42+
def entity_id() -> str:
43+
return ENTITY_ID
44+
45+
46+
@pytest.fixture(scope="module")
47+
def dataset(client: bigquery.Client) -> Dataset:
48+
dataset = client.create_dataset(DATASET_ID)
49+
yield dataset
50+
client.delete_dataset(dataset, delete_contents=True)
51+
52+
53+
@pytest.fixture(scope="module")
54+
def table(client: bigquery.Client, project_id: str) -> Table:
55+
FULL_TABLE_NAME = f"{project_id}.{DATASET_ID}.{TABLE_NAME}"
56+
57+
sample_schema = [
58+
bigquery.SchemaField("id", "INTEGER", mode="REQUIRED"),
59+
]
60+
61+
table = bigquery.Table(FULL_TABLE_NAME, schema=sample_schema)
62+
client.create_table(table)
63+
64+
return table
65+
66+
67+
@pytest.fixture()
68+
def view(client: bigquery.Client, project_id: str, table: str) -> str:
69+
FULL_VIEW_NAME = f"{project_id}.{DATASET_ID}.{VIEW_NAME}"
70+
view = bigquery.Table(FULL_VIEW_NAME)
71+
72+
# f"{table}" will inject the full table name,
73+
# with project_id and dataset_id, as required by
74+
# .create_table()
75+
view.view_query = f"SELECT * FROM `{table}`"
76+
view = client.create_table(view)
77+
return view
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Copyright 2025 Google LLC
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+
# https://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+
from google.cloud.bigquery.dataset import AccessEntry
16+
17+
18+
def grant_access_to_dataset(
19+
dataset_id: str,
20+
entity_id: str,
21+
role: str
22+
) -> list[AccessEntry]:
23+
# [START bigquery_grant_access_to_dataset]
24+
from google.api_core.exceptions import PreconditionFailed
25+
from google.cloud import bigquery
26+
from google.cloud.bigquery.enums import EntityTypes
27+
28+
# TODO(developer): Update and un-comment below lines
29+
30+
# ID of the dataset to revoke access to.
31+
# dataset_id = "my_project_id.my_dataset"
32+
33+
# ID of the user or group from whom you are adding access.
34+
# Alternatively, the JSON REST API representation of the entity,
35+
# such as a view's table reference.
36+
# entity_id = "user-or-group-to-add@example.com"
37+
38+
# One of the "Basic roles for datasets" described here:
39+
# https://cloud.google.com/bigquery/docs/access-control-basic-roles#dataset-basic-roles
40+
# role = "READER"
41+
42+
# Type of entity you are granting access to.
43+
# Find allowed allowed entity type names here:
44+
# https://cloud.google.com/python/docs/reference/bigquery/latest/enums#class-googlecloudbigqueryenumsentitytypesvalue
45+
entity_type = EntityTypes.GROUP_BY_EMAIL
46+
47+
# Instantiate a client.
48+
client = bigquery.Client()
49+
50+
# Get a reference to the dataset.
51+
dataset = client.get_dataset(dataset_id)
52+
53+
# The `access_entries` list is immutable. Create a copy for modifications.
54+
entries = list(dataset.access_entries)
55+
56+
# Append an AccessEntry to grant the role to a dataset.
57+
# Find more details about the AccessEntry object here:
58+
# https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.dataset.AccessEntry
59+
entries.append(
60+
bigquery.AccessEntry(
61+
role=role,
62+
entity_type=entity_type,
63+
entity_id=entity_id,
64+
)
65+
)
66+
67+
# Assign the list of AccessEntries back to the dataset.
68+
dataset.access_entries = entries
69+
70+
# Update will only succeed if the dataset
71+
# has not been modified externally since retrieval.
72+
#
73+
# See the BigQuery client library documentation for more details on `update_dataset`:
74+
# https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.client.Client#google_cloud_bigquery_client_Client_update_dataset
75+
try:
76+
# Update just the `access_entries` property of the dataset.
77+
dataset = client.update_dataset(
78+
dataset,
79+
["access_entries"],
80+
)
81+
82+
# Show a success message.
83+
full_dataset_id = f"{dataset.project}.{dataset.dataset_id}"
84+
print(
85+
f"Role '{role}' granted for entity '{entity_id}'"
86+
f" in dataset '{full_dataset_id}'."
87+
)
88+
except PreconditionFailed: # A read-modify-write error
89+
print(
90+
f"Dataset '{dataset.dataset_id}' was modified remotely before this update. "
91+
"Fetch the latest version and retry."
92+
)
93+
# [END bigquery_grant_access_to_dataset]
94+
95+
return dataset.access_entries
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright 2025 Google LLC
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+
# https://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+
from google.cloud.bigquery.dataset import Dataset
16+
17+
from grant_access_to_dataset import grant_access_to_dataset
18+
19+
20+
def test_grant_access_to_dataset(
21+
dataset: Dataset,
22+
entity_id: str
23+
) -> None:
24+
dataset_access_entries = grant_access_to_dataset(
25+
dataset_id=dataset.dataset_id,
26+
entity_id=entity_id,
27+
role="READER"
28+
)
29+
30+
updated_dataset_entity_ids = {
31+
entry.entity_id for entry in dataset_access_entries
32+
}
33+
assert entity_id in updated_dataset_entity_ids
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright 2024 Google LLC
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+
# https://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+
from google.api_core.iam import Policy
16+
17+
18+
def grant_access_to_table_or_view(
19+
project_id: str,
20+
dataset_id: str,
21+
resource_name: str,
22+
principal_id: str,
23+
role: str,
24+
) -> Policy:
25+
26+
# [START bigquery_grant_access_to_table_or_view]
27+
from google.cloud import bigquery
28+
29+
# TODO(developer): Update and un-comment below lines
30+
31+
# Google Cloud Platform project.
32+
# project_id = "my_project_id"
33+
34+
# Dataset where the table or view is.
35+
# dataset_id = "my_dataset"
36+
37+
# Table or view name to get the access policy.
38+
# resource_name = "my_table"
39+
40+
# The principal requesting access to the table or view.
41+
# Find more details about principal identifiers here:
42+
# https://cloud.google.com/iam/docs/principal-identifiers
43+
# principal_id = "user:bob@example.com"
44+
45+
# Role to assign to the member.
46+
# role = "roles/bigquery.dataViewer"
47+
48+
# Instantiate a client.
49+
client = bigquery.Client()
50+
51+
# Get the full table or view name.
52+
full_resource_name = f"{project_id}.{dataset_id}.{resource_name}"
53+
54+
# Get the IAM access policy for the table or view.
55+
policy = client.get_iam_policy(full_resource_name)
56+
57+
# To grant access to a table or view,
58+
# add bindings to the Table or View policy.
59+
#
60+
# Find more details about Policy and Binding objects here:
61+
# https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Policy
62+
# https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Binding
63+
binding = {
64+
"role": role,
65+
"members": [principal_id, ],
66+
}
67+
policy.bindings.append(binding)
68+
69+
# Set the IAM acces spolicy with updated bindings
70+
updated_policy = client.set_iam_policy(full_resource_name, policy)
71+
72+
# Show a success message.
73+
print(
74+
f"Role '{role}' granted for principal '{principal_id}'"
75+
f" on resource '{full_resource_name}'."
76+
)
77+
# [END bigquery_grant_access_to_table_or_view]
78+
79+
return updated_policy.bindings
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright 2025 Google LLC
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+
# https://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+
from google.cloud import bigquery
16+
from google.cloud.bigquery.dataset import Dataset
17+
from google.cloud.bigquery.table import Table
18+
19+
from grant_access_to_table_or_view import grant_access_to_table_or_view
20+
21+
22+
def test_grant_access_to_table_or_view(
23+
client: bigquery.Client,
24+
dataset: Dataset,
25+
project_id: str,
26+
table: Table,
27+
entity_id: str,
28+
) -> None:
29+
ROLE = "roles/bigquery.dataViewer"
30+
PRINCIPAL_ID = f"group:{entity_id}"
31+
32+
empty_policy = client.get_iam_policy(table)
33+
34+
# In an empty policy the role and principal is not present
35+
assert not any(p for p in empty_policy if p["role"] == ROLE)
36+
assert not any(p for p in empty_policy if PRINCIPAL_ID in p["members"])
37+
38+
updated_policy = grant_access_to_table_or_view(
39+
project_id,
40+
dataset.dataset_id,
41+
table.table_id,
42+
principal_id=PRINCIPAL_ID,
43+
role=ROLE,
44+
)
45+
46+
# A binding with that role exists
47+
assert any(p for p in updated_policy if p["role"] == ROLE)
48+
# A binding for that principal exists
49+
assert any(
50+
p for p in updated_policy
51+
if PRINCIPAL_ID in p["members"]
52+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# samples/snippets should be runnable with no "extras"
2+
google-cloud-testutils==1.5.0
3+
pytest==8.3.4
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# samples/snippets should be runnable with no "extras"
2+
google-cloud-bigquery==3.29.0

0 commit comments

Comments
 (0)