diff --git a/bigquery/google/cloud/bigquery/dataset.py b/bigquery/google/cloud/bigquery/dataset.py index 8566e183cda00..01260ccc6e686 100644 --- a/bigquery/google/cloud/bigquery/dataset.py +++ b/bigquery/google/cloud/bigquery/dataset.py @@ -22,6 +22,7 @@ import google.cloud._helpers from google.cloud.bigquery import _helpers from google.cloud.bigquery.model import ModelReference +from google.cloud.bigquery.routine import RoutineReference from google.cloud.bigquery.table import TableReference @@ -53,6 +54,25 @@ def _get_model_reference(self, model_id): ) +def _get_routine_reference(self, routine_id): + """Constructs a RoutineReference. + + Args: + routine_id (str): the ID of the routine. + + Returns: + google.cloud.bigquery.routine.RoutineReference: + A RoutineReference for a routine in this dataset. + """ + return RoutineReference.from_api_repr( + { + "projectId": self.project, + "datasetId": self.dataset_id, + "routineId": routine_id, + } + ) + + class AccessEntry(object): """Represents grant of an access role to an entity. @@ -224,6 +244,8 @@ def path(self): model = _get_model_reference + routine = _get_routine_reference + @classmethod def from_api_repr(cls, resource): """Factory: construct a dataset reference given its API representation @@ -591,6 +613,8 @@ def _build_resource(self, filter_fields): model = _get_model_reference + routine = _get_routine_reference + def __repr__(self): return "Dataset({})".format(repr(self.reference)) @@ -672,3 +696,5 @@ def reference(self): table = _get_table_reference model = _get_model_reference + + routine = _get_routine_reference diff --git a/bigquery/google/cloud/bigquery/routine.py b/bigquery/google/cloud/bigquery/routine.py index c7136d78c422c..58cf693dfb8ae 100644 --- a/bigquery/google/cloud/bigquery/routine.py +++ b/bigquery/google/cloud/bigquery/routine.py @@ -189,15 +189,19 @@ def return_type(self): https://cloud.google.com/bigquery/docs/reference/rest/v2/routines#resource-routine """ resource = self._properties.get(self._PROPERTY_TO_API_FIELD["return_type"]) + if not resource: + return resource output = google.cloud.bigquery_v2.types.StandardSqlDataType() output = json_format.ParseDict(resource, output, ignore_unknown_fields=True) return output @return_type.setter def return_type(self, value): - self._properties[ - self._PROPERTY_TO_API_FIELD["return_type"] - ] = json_format.MessageToDict(value) + if value: + resource = json_format.MessageToDict(value) + else: + resource = None + self._properties[self._PROPERTY_TO_API_FIELD["return_type"]] = resource @property def imported_libraries(self): @@ -257,6 +261,11 @@ def _build_resource(self, filter_fields): """Generate a resource for ``update``.""" return _helpers._build_resource_from_properties(self, filter_fields) + def __repr__(self): + return "Routine('{}.{}.{}')".format( + self.project, self.dataset_id, self.routine_id + ) + class RoutineArgument(object): """Input/output argument of a function or a stored procedure. @@ -328,15 +337,19 @@ def data_type(self): https://cloud.google.com/bigquery/docs/reference/rest/v2/StandardSqlDataType """ resource = self._properties.get(self._PROPERTY_TO_API_FIELD["data_type"]) + if not resource: + return resource output = google.cloud.bigquery_v2.types.StandardSqlDataType() output = json_format.ParseDict(resource, output, ignore_unknown_fields=True) return output @data_type.setter def data_type(self, value): - self._properties[ - self._PROPERTY_TO_API_FIELD["data_type"] - ] = json_format.MessageToDict(value) + if value: + resource = json_format.MessageToDict(value) + else: + resource = None + self._properties[self._PROPERTY_TO_API_FIELD["data_type"]] = resource @classmethod def from_api_repr(cls, resource): @@ -354,15 +367,6 @@ def from_api_repr(cls, resource): ref._properties = resource return ref - def to_api_repr(self): - """Construct the API resource representation of this routine argument. - - Returns: - Dict[str, object]: - Routine argument represented as an API resource. - """ - return self._properties - def __eq__(self, other): if not isinstance(other, RoutineArgument): return NotImplemented @@ -374,7 +378,7 @@ def __ne__(self, other): def __repr__(self): all_properties = [ "{}={}".format(property_name, repr(getattr(self, property_name))) - for property_name in self._PROPERTY_TO_API_FIELD + for property_name in sorted(self._PROPERTY_TO_API_FIELD) ] return "RoutineArgument({})".format(", ".join(all_properties)) @@ -458,15 +462,6 @@ def from_string(cls, routine_id, default_project=None): {"projectId": proj, "datasetId": dset, "routineId": routine} ) - def to_api_repr(self): - """Construct the API resource representation of this routine reference. - - Returns: - Dict[str, object]: - Routine reference represented as an API resource. - """ - return self._properties - def __eq__(self, other): """Two RoutineReferences are equal if they point to the same routine.""" if not isinstance(other, RoutineReference): diff --git a/bigquery/samples/update_routine.py b/bigquery/samples/update_routine.py index 9fc3c877120f8..8683e761562f9 100644 --- a/bigquery/samples/update_routine.py +++ b/bigquery/samples/update_routine.py @@ -35,6 +35,7 @@ def main(client, routine_id): # Due to a limitation of the API, all fields are required, not just # those that have been updated. "arguments", + "language", "type_", "return_type", ], diff --git a/bigquery/tests/unit/routine/__init__.py b/bigquery/tests/unit/routine/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/bigquery/tests/unit/routine/test_routine.py b/bigquery/tests/unit/routine/test_routine.py new file mode 100644 index 0000000000000..fded11ef88f7d --- /dev/null +++ b/bigquery/tests/unit/routine/test_routine.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime + +import pytest + +import google.cloud._helpers +from google.cloud import bigquery_v2 + + +@pytest.fixture +def target_class(): + from google.cloud.bigquery.routine import Routine + + return Routine + + +@pytest.fixture +def object_under_test(target_class): + return target_class("project-id.dataset_id.routine_id") + + +def test_ctor(target_class): + from google.cloud.bigquery.routine import RoutineReference + + ref = RoutineReference.from_string("my-proj.my_dset.my_routine") + actual_routine = target_class(ref) + assert actual_routine.reference == ref + assert ( + actual_routine.path == "/projects/my-proj/datasets/my_dset/routines/my_routine" + ) + + +def test_ctor_w_string(target_class): + from google.cloud.bigquery.routine import RoutineReference + + routine_id = "my-proj.my_dset.my_routine" + ref = RoutineReference.from_string(routine_id) + actual_routine = target_class(routine_id) + assert actual_routine.reference == ref + + +def test_ctor_w_properties(target_class): + from google.cloud.bigquery.routine import RoutineArgument + from google.cloud.bigquery.routine import RoutineReference + + routine_id = "my-proj.my_dset.my_routine" + arguments = [ + RoutineArgument( + name="x", + data_type=bigquery_v2.types.StandardSqlDataType( + type_kind=bigquery_v2.enums.StandardSqlDataType.TypeKind.INT64 + ), + ) + ] + body = "x * 3" + language = "SQL" + return_type = bigquery_v2.types.StandardSqlDataType( + type_kind=bigquery_v2.enums.StandardSqlDataType.TypeKind.INT64 + ) + type_ = "SCALAR_FUNCTION" + + actual_routine = target_class( + routine_id, + arguments=arguments, + body=body, + language=language, + return_type=return_type, + type_=type_, + ) + + ref = RoutineReference.from_string(routine_id) + assert actual_routine.reference == ref + assert actual_routine.arguments == arguments + assert actual_routine.body == body + assert actual_routine.language == language + assert actual_routine.return_type == return_type + assert actual_routine.type_ == type_ + + +def test_from_api_repr(target_class): + from google.cloud.bigquery.routine import RoutineArgument + from google.cloud.bigquery.routine import RoutineReference + + creation_time = datetime.datetime( + 2010, 5, 19, 16, 0, 0, tzinfo=google.cloud._helpers.UTC + ) + modified_time = datetime.datetime( + 2011, 10, 1, 16, 0, 0, tzinfo=google.cloud._helpers.UTC + ) + resource = { + "routineReference": { + "projectId": "my-project", + "datasetId": "my_dataset", + "routineId": "my_routine", + }, + "etag": "abcdefg", + "creationTime": str(google.cloud._helpers._millis(creation_time)), + "lastModifiedTime": str(google.cloud._helpers._millis(modified_time)), + "definitionBody": "42", + "arguments": [{"name": "x", "dataType": {"typeKind": "INT64"}}], + "language": "SQL", + "returnType": {"typeKind": "INT64"}, + "routineType": "SCALAR_FUNCTION", + "someNewField": "someValue", + } + actual_routine = target_class.from_api_repr(resource) + + assert actual_routine.project == "my-project" + assert actual_routine.dataset_id == "my_dataset" + assert actual_routine.routine_id == "my_routine" + assert ( + actual_routine.path + == "/projects/my-project/datasets/my_dataset/routines/my_routine" + ) + assert actual_routine.reference == RoutineReference.from_string( + "my-project.my_dataset.my_routine" + ) + assert actual_routine.etag == "abcdefg" + assert actual_routine.created == creation_time + assert actual_routine.modified == modified_time + assert actual_routine.arguments == [ + RoutineArgument( + name="x", + data_type=bigquery_v2.types.StandardSqlDataType( + type_kind=bigquery_v2.enums.StandardSqlDataType.TypeKind.INT64 + ), + ) + ] + assert actual_routine.body == "42" + assert actual_routine.language == "SQL" + assert actual_routine.return_type == bigquery_v2.types.StandardSqlDataType( + type_kind=bigquery_v2.enums.StandardSqlDataType.TypeKind.INT64 + ) + assert actual_routine.type_ == "SCALAR_FUNCTION" + assert actual_routine._properties["someNewField"] == "someValue" + + +def test_from_api_repr_w_minimal_resource(target_class): + from google.cloud.bigquery.routine import RoutineReference + + resource = { + "routineReference": { + "projectId": "my-project", + "datasetId": "my_dataset", + "routineId": "my_routine", + } + } + actual_routine = target_class.from_api_repr(resource) + assert actual_routine.reference == RoutineReference.from_string( + "my-project.my_dataset.my_routine" + ) + assert actual_routine.etag is None + assert actual_routine.created is None + assert actual_routine.modified is None + assert actual_routine.arguments == [] + assert actual_routine.body is None + assert actual_routine.language is None + assert actual_routine.return_type is None + assert actual_routine.type_ is None + + +def test_from_api_repr_w_unknown_fields(target_class): + from google.cloud.bigquery.routine import RoutineReference + + resource = { + "routineReference": { + "projectId": "my-project", + "datasetId": "my_dataset", + "routineId": "my_routine", + }, + "thisFieldIsNotInTheProto": "just ignore me", + } + actual_routine = target_class.from_api_repr(resource) + assert actual_routine.reference == RoutineReference.from_string( + "my-project.my_dataset.my_routine" + ) + assert actual_routine._properties is resource + + +@pytest.mark.parametrize( + "resource,filter_fields,expected", + [ + ( + { + "arguments": [{"name": "x", "dataType": {"typeKind": "INT64"}}], + "definitionBody": "x * 3", + "language": "SQL", + "returnType": {"typeKind": "INT64"}, + "routineType": "SCALAR_FUNCTION", + }, + ["arguments"], + {"arguments": [{"name": "x", "dataType": {"typeKind": "INT64"}}]}, + ), + ( + { + "arguments": [{"name": "x", "dataType": {"typeKind": "INT64"}}], + "definitionBody": "x * 3", + "language": "SQL", + "returnType": {"typeKind": "INT64"}, + "routineType": "SCALAR_FUNCTION", + }, + ["body"], + {"definitionBody": "x * 3"}, + ), + ( + { + "arguments": [{"name": "x", "dataType": {"typeKind": "INT64"}}], + "definitionBody": "x * 3", + "language": "SQL", + "returnType": {"typeKind": "INT64"}, + "routineType": "SCALAR_FUNCTION", + }, + ["language"], + {"language": "SQL"}, + ), + ( + { + "arguments": [{"name": "x", "dataType": {"typeKind": "INT64"}}], + "definitionBody": "x * 3", + "language": "SQL", + "returnType": {"typeKind": "INT64"}, + "routineType": "SCALAR_FUNCTION", + }, + ["return_type"], + {"returnType": {"typeKind": "INT64"}}, + ), + ( + { + "arguments": [{"name": "x", "dataType": {"typeKind": "INT64"}}], + "definitionBody": "x * 3", + "language": "SQL", + "returnType": {"typeKind": "INT64"}, + "routineType": "SCALAR_FUNCTION", + }, + ["type_"], + {"routineType": "SCALAR_FUNCTION"}, + ), + ( + {}, + ["arguments", "language", "body", "type_", "return_type"], + { + "arguments": None, + "definitionBody": None, + "language": None, + "returnType": None, + "routineType": None, + }, + ), + ( + {"someNewField": "someValue"}, + ["someNewField"], + {"someNewField": "someValue"}, + ), + ], +) +def test_build_resource(object_under_test, resource, filter_fields, expected): + object_under_test._properties = resource + actual_routine = object_under_test._build_resource(filter_fields) + assert actual_routine == expected + + +def test_repr(target_class): + model = target_class("my-proj.my_dset.my_routine") + actual_routine = repr(model) + assert actual_routine == "Routine('my-proj.my_dset.my_routine')" diff --git a/bigquery/tests/unit/routine/test_routine_argument.py b/bigquery/tests/unit/routine/test_routine_argument.py new file mode 100644 index 0000000000000..1d7839afbb992 --- /dev/null +++ b/bigquery/tests/unit/routine/test_routine_argument.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from google.cloud import bigquery_v2 + + +@pytest.fixture +def target_class(): + from google.cloud.bigquery.routine import RoutineArgument + + return RoutineArgument + + +def test_ctor(target_class): + data_type = bigquery_v2.types.StandardSqlDataType( + type_kind=bigquery_v2.enums.StandardSqlDataType.TypeKind.INT64 + ) + actual_arg = target_class( + name="field_name", kind="FIXED_TYPE", mode="IN", data_type=data_type + ) + assert actual_arg.name == "field_name" + assert actual_arg.kind == "FIXED_TYPE" + assert actual_arg.mode == "IN" + assert actual_arg.data_type == data_type + + +def test_from_api_repr(target_class): + resource = { + "argumentKind": "FIXED_TYPE", + "dataType": {"typeKind": "INT64"}, + "mode": "IN", + "name": "field_name", + } + actual_arg = target_class.from_api_repr(resource) + assert actual_arg.name == "field_name" + assert actual_arg.kind == "FIXED_TYPE" + assert actual_arg.mode == "IN" + assert actual_arg.data_type == bigquery_v2.types.StandardSqlDataType( + type_kind=bigquery_v2.enums.StandardSqlDataType.TypeKind.INT64 + ) + + +def test_from_api_repr_w_minimal_resource(target_class): + resource = {} + actual_arg = target_class.from_api_repr(resource) + assert actual_arg.name is None + assert actual_arg.kind is None + assert actual_arg.mode is None + assert actual_arg.data_type is None + + +def test_from_api_repr_w_unknown_fields(target_class): + resource = {"thisFieldIsNotInTheProto": "just ignore me"} + actual_arg = target_class.from_api_repr(resource) + assert actual_arg._properties is resource + + +def test_repr(target_class): + arg = target_class(name="field_name", kind="FIXED_TYPE", mode="IN", data_type=None) + actual_repr = repr(arg) + assert actual_repr == ( + "RoutineArgument(data_type=None, kind='FIXED_TYPE', mode='IN', name='field_name')" + ) diff --git a/bigquery/tests/unit/routine/test_routine_reference.py b/bigquery/tests/unit/routine/test_routine_reference.py new file mode 100644 index 0000000000000..9d3d551a6294e --- /dev/null +++ b/bigquery/tests/unit/routine/test_routine_reference.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + + +@pytest.fixture +def target_class(): + from google.cloud.bigquery.routine import RoutineReference + + return RoutineReference + + +def test_from_api_repr(target_class): + resource = { + "projectId": "my-project", + "datasetId": "my_dataset", + "routineId": "my_routine", + } + got = target_class.from_api_repr(resource) + assert got.project == "my-project" + assert got.dataset_id == "my_dataset" + assert got.routine_id == "my_routine" + assert got.path == "/projects/my-project/datasets/my_dataset/routines/my_routine" + + +def test_from_api_repr_w_unknown_fields(target_class): + resource = { + "projectId": "my-project", + "datasetId": "my_dataset", + "routineId": "my_routine", + "thisFieldIsNotInTheProto": "just ignore me", + } + got = target_class.from_api_repr(resource) + assert got.project == "my-project" + assert got.dataset_id == "my_dataset" + assert got.routine_id == "my_routine" + assert got._properties is resource + + +def test_to_api_repr(target_class): + ref = target_class.from_string("my-project.my_dataset.my_routine") + got = ref.to_api_repr() + assert got == { + "projectId": "my-project", + "datasetId": "my_dataset", + "routineId": "my_routine", + } + + +def test_from_string(target_class): + got = target_class.from_string("string-project.string_dataset.string_routine") + assert got.project == "string-project" + assert got.dataset_id == "string_dataset" + assert got.routine_id == "string_routine" + assert got.path == ( + "/projects/string-project/datasets/string_dataset/routines/string_routine" + ) + + +def test_from_string_legacy_string(target_class): + with pytest.raises(ValueError): + target_class.from_string("string-project:string_dataset.string_routine") + + +def test_from_string_not_fully_qualified(target_class): + with pytest.raises(ValueError): + target_class.from_string("string_routine") + + with pytest.raises(ValueError): + target_class.from_string("string_dataset.string_routine") + + with pytest.raises(ValueError): + target_class.from_string("a.b.c.d") + + +def test_from_string_with_default_project(target_class): + got = target_class.from_string( + "string_dataset.string_routine", default_project="default-project" + ) + assert got.project == "default-project" + assert got.dataset_id == "string_dataset" + assert got.routine_id == "string_routine" + + +def test_from_string_ignores_default_project(target_class): + got = target_class.from_string( + "string-project.string_dataset.string_routine", + default_project="default-project", + ) + assert got.project == "string-project" + assert got.dataset_id == "string_dataset" + assert got.routine_id == "string_routine" + + +def test_eq(target_class): + routine = target_class.from_string("my-proj.my_dset.my_routine") + routine_too = target_class.from_string("my-proj.my_dset.my_routine") + assert routine == routine_too + assert not (routine != routine_too) + + other_routine = target_class.from_string("my-proj.my_dset.my_routine2") + assert not (routine == other_routine) + assert routine != other_routine + + notaroutine = object() + assert not (routine == notaroutine) + assert routine != notaroutine + + +def test_hash(target_class): + routine = target_class.from_string("my-proj.my_dset.my_routine") + routine2 = target_class.from_string("my-proj.my_dset.routine2") + got = {routine: "hello", routine2: "world"} + assert got[routine] == "hello" + assert got[routine2] == "world" + + routine_too = target_class.from_string("my-proj.my_dset.my_routine") + assert got[routine_too] == "hello" + + +def test_repr(target_class): + routine = target_class.from_string("my-proj.my_dset.my_routine") + got = repr(routine) + assert got == "RoutineReference.from_string('my-proj.my_dset.my_routine')" diff --git a/bigquery/tests/unit/test_client.py b/bigquery/tests/unit/test_client.py index 0eebdcbe0f502..8ad9dc8858c6b 100644 --- a/bigquery/tests/unit/test_client.py +++ b/bigquery/tests/unit/test_client.py @@ -41,8 +41,9 @@ import google.api_core.exceptions from google.api_core.gapic_v1 import client_info import google.cloud._helpers -from tests.unit.helpers import make_connection +from google.cloud import bigquery_v2 from google.cloud.bigquery.dataset import DatasetReference +from tests.unit.helpers import make_connection def _make_credentials(): @@ -894,6 +895,70 @@ def test_create_routine_w_minimal_resource(self): actual_routine.reference, RoutineReference.from_string(full_routine_id) ) + def test_create_routine_w_conflict(self): + from google.cloud.bigquery.routine import Routine + + creds = _make_credentials() + client = self._make_one(project=self.PROJECT, credentials=creds) + conn = client._connection = make_connection( + google.api_core.exceptions.AlreadyExists("routine already exists") + ) + full_routine_id = "test-routine-project.test_routines.minimal_routine" + routine = Routine(full_routine_id) + + with pytest.raises(google.api_core.exceptions.AlreadyExists): + client.create_routine(routine) + + resource = { + "routineReference": { + "projectId": "test-routine-project", + "datasetId": "test_routines", + "routineId": "minimal_routine", + } + } + conn.api_request.assert_called_once_with( + method="POST", + path="/projects/test-routine-project/datasets/test_routines/routines", + data=resource, + ) + + def test_create_routine_w_conflict_exists_ok(self): + from google.cloud.bigquery.routine import Routine + + creds = _make_credentials() + client = self._make_one(project=self.PROJECT, credentials=creds) + resource = { + "routineReference": { + "projectId": "test-routine-project", + "datasetId": "test_routines", + "routineId": "minimal_routine", + } + } + conn = client._connection = make_connection( + google.api_core.exceptions.AlreadyExists("routine already exists"), resource + ) + full_routine_id = "test-routine-project.test_routines.minimal_routine" + routine = Routine(full_routine_id) + + actual_routine = client.create_routine(routine, exists_ok=True) + + self.assertEqual(actual_routine.project, "test-routine-project") + self.assertEqual(actual_routine.dataset_id, "test_routines") + self.assertEqual(actual_routine.routine_id, "minimal_routine") + conn.api_request.assert_has_calls( + [ + mock.call( + method="POST", + path="/projects/test-routine-project/datasets/test_routines/routines", + data=resource, + ), + mock.call( + method="GET", + path="/projects/test-routine-project/datasets/test_routines/routines/minimal_routine", + ), + ] + ) + def test_create_table_w_day_partition(self): from google.cloud.bigquery.table import Table from google.cloud.bigquery.table import TimePartitioning @@ -1326,6 +1391,52 @@ def test_get_model_w_string(self): conn.api_request.assert_called_once_with(method="GET", path="/%s" % path) self.assertEqual(got.model_id, self.MODEL_ID) + def test_get_routine(self): + from google.cloud.bigquery.routine import Routine + from google.cloud.bigquery.routine import RoutineReference + + full_routine_id = "test-routine-project.test_routines.minimal_routine" + routines = [ + full_routine_id, + Routine(full_routine_id), + RoutineReference.from_string(full_routine_id), + ] + for routine in routines: + creds = _make_credentials() + resource = { + "etag": "im-an-etag", + "routineReference": { + "projectId": "test-routine-project", + "datasetId": "test_routines", + "routineId": "minimal_routine", + }, + "routineType": "SCALAR_FUNCTION", + } + client = self._make_one(project=self.PROJECT, credentials=creds) + conn = client._connection = make_connection(resource) + + actual_routine = client.get_routine(routine) + + conn.api_request.assert_called_once_with( + method="GET", + path="/projects/test-routine-project/datasets/test_routines/routines/minimal_routine", + ) + self.assertEqual( + actual_routine.reference, + RoutineReference.from_string(full_routine_id), + msg="routine={}".format(repr(routine)), + ) + self.assertEqual( + actual_routine.etag, + "im-an-etag", + msg="routine={}".format(repr(routine)), + ) + self.assertEqual( + actual_routine.type_, + "SCALAR_FUNCTION", + msg="routine={}".format(repr(routine)), + ) + def test_get_table(self): path = "projects/%s/datasets/%s/tables/%s" % ( self.PROJECT, @@ -1527,6 +1638,66 @@ def test_update_model(self): req = conn.api_request.call_args self.assertEqual(req[1]["headers"]["If-Match"], "etag") + def test_update_routine(self): + from google.cloud.bigquery.routine import Routine + from google.cloud.bigquery.routine import RoutineArgument + + full_routine_id = "routines-project.test_routines.updated_routine" + resource = { + "routineReference": { + "projectId": "routines-project", + "datasetId": "test_routines", + "routineId": "updated_routine", + }, + "routineType": "SCALAR_FUNCTION", + "language": "SQL", + "definitionBody": "x * 3", + "arguments": [{"name": "x", "dataType": {"typeKind": "INT64"}}], + "returnType": None, + "someNewField": "someValue", + } + creds = _make_credentials() + client = self._make_one(project=self.PROJECT, credentials=creds) + conn = client._connection = make_connection(resource, resource) + routine = Routine(full_routine_id) + routine.arguments = [ + RoutineArgument( + name="x", + data_type=bigquery_v2.types.StandardSqlDataType( + type_kind=bigquery_v2.enums.StandardSqlDataType.TypeKind.INT64 + ), + ) + ] + routine.body = "x * 3" + routine.language = "SQL" + routine.type_ = "SCALAR_FUNCTION" + routine._properties["someNewField"] = "someValue" + + actual_routine = client.update_routine( + routine, + ["arguments", "language", "body", "type_", "return_type", "someNewField"], + ) + + # TODO: routineReference isn't needed when the Routines API supports + # partial updates. + sent = resource + conn.api_request.assert_called_once_with( + method="PUT", + data=sent, + path="/projects/routines-project/datasets/test_routines/routines/updated_routine", + headers=None, + ) + self.assertEqual(actual_routine.arguments, routine.arguments) + self.assertEqual(actual_routine.body, routine.body) + self.assertEqual(actual_routine.language, routine.language) + self.assertEqual(actual_routine.type_, routine.type_) + + # ETag becomes If-Match header. + routine._properties["etag"] = "im-an-etag" + client.update_routine(routine, []) + req = conn.api_request.call_args + self.assertEqual(req[1]["headers"]["If-Match"], "im-an-etag") + def test_update_table(self): from google.cloud.bigquery.table import Table, SchemaField @@ -1905,6 +2076,82 @@ def test_list_models_wrong_type(self): with self.assertRaises(TypeError): client.list_models(client.dataset(self.DS_ID).model("foo")) + def test_list_routines_empty(self): + creds = _make_credentials() + client = self._make_one(project=self.PROJECT, credentials=creds) + conn = client._connection = make_connection({}) + + iterator = client.list_routines("test-routines.test_routines") + page = six.next(iterator.pages) + routines = list(page) + token = iterator.next_page_token + + self.assertEqual(routines, []) + self.assertIsNone(token) + conn.api_request.assert_called_once_with( + method="GET", + path="/projects/test-routines/datasets/test_routines/routines", + query_params={}, + ) + + def test_list_routines_defaults(self): + from google.cloud.bigquery.routine import Routine + + project_id = "test-routines" + dataset_id = "test_routines" + path = "/projects/test-routines/datasets/test_routines/routines" + routine_1 = "routine_one" + routine_2 = "routine_two" + token = "TOKEN" + resource = { + "nextPageToken": token, + "routines": [ + { + "routineReference": { + "routineId": routine_1, + "datasetId": dataset_id, + "projectId": project_id, + } + }, + { + "routineReference": { + "routineId": routine_2, + "datasetId": dataset_id, + "projectId": project_id, + } + }, + ], + } + + creds = _make_credentials() + client = self._make_one(project=project_id, credentials=creds) + conn = client._connection = make_connection(resource) + dataset = client.dataset(dataset_id) + + iterator = client.list_routines(dataset) + self.assertIs(iterator.dataset, dataset) + page = six.next(iterator.pages) + routines = list(page) + actual_token = iterator.next_page_token + + self.assertEqual(len(routines), len(resource["routines"])) + for found, expected in zip(routines, resource["routines"]): + self.assertIsInstance(found, Routine) + self.assertEqual( + found.routine_id, expected["routineReference"]["routineId"] + ) + self.assertEqual(actual_token, token) + + conn.api_request.assert_called_once_with( + method="GET", path=path, query_params={} + ) + + def test_list_routines_wrong_type(self): + creds = _make_credentials() + client = self._make_one(project=self.PROJECT, credentials=creds) + with self.assertRaises(TypeError): + client.list_routines(client.dataset(self.DS_ID).table("foo")) + def test_list_tables_defaults(self): from google.cloud.bigquery.table import TableListItem @@ -2154,6 +2401,67 @@ def test_delete_model_w_not_found_ok_true(self): conn.api_request.assert_called_with(method="DELETE", path=path) + def test_delete_routine(self): + from google.cloud.bigquery.routine import Routine + from google.cloud.bigquery.routine import RoutineReference + + full_routine_id = "test-routine-project.test_routines.minimal_routine" + routines = [ + full_routine_id, + Routine(full_routine_id), + RoutineReference.from_string(full_routine_id), + ] + creds = _make_credentials() + http = object() + client = self._make_one(project=self.PROJECT, credentials=creds, _http=http) + conn = client._connection = make_connection(*([{}] * len(routines))) + + for routine in routines: + client.delete_routine(routine) + conn.api_request.assert_called_with( + method="DELETE", + path="/projects/test-routine-project/datasets/test_routines/routines/minimal_routine", + ) + + def test_delete_routine_w_wrong_type(self): + creds = _make_credentials() + client = self._make_one(project=self.PROJECT, credentials=creds) + with self.assertRaises(TypeError): + client.delete_routine(client.dataset(self.DS_ID)) + + def test_delete_routine_w_not_found_ok_false(self): + creds = _make_credentials() + http = object() + client = self._make_one(project=self.PROJECT, credentials=creds, _http=http) + conn = client._connection = make_connection( + google.api_core.exceptions.NotFound("routine not found") + ) + + with self.assertRaises(google.api_core.exceptions.NotFound): + client.delete_routine("routines-project.test_routines.test_routine") + + conn.api_request.assert_called_with( + method="DELETE", + path="/projects/routines-project/datasets/test_routines/routines/test_routine", + ) + + def test_delete_routine_w_not_found_ok_true(self): + creds = _make_credentials() + http = object() + client = self._make_one(project=self.PROJECT, credentials=creds, _http=http) + conn = client._connection = make_connection( + google.api_core.exceptions.NotFound("routine not found") + ) + + client.delete_routine( + "routines-project.test_routines.test_routine", not_found_ok=True + ) + + conn.api_request.assert_called_with( + method="DELETE", + path="/projects/routines-project/datasets/test_routines/routines/test_routine", + ) + def test_delete_table(self): from google.cloud.bigquery.table import Table diff --git a/bigquery/tests/unit/test_dataset.py b/bigquery/tests/unit/test_dataset.py index 96a2ace7da0c4..b8805a9c7ce39 100644 --- a/bigquery/tests/unit/test_dataset.py +++ b/bigquery/tests/unit/test_dataset.py @@ -158,6 +158,13 @@ def test_model(self): self.assertEqual(model_ref.dataset_id, "dataset_1") self.assertEqual(model_ref.model_id, "model_1") + def test_routine(self): + dataset_ref = self._make_one("some-project-1", "dataset_1") + routine_ref = dataset_ref.routine("routine_1") + self.assertEqual(routine_ref.project, "some-project-1") + self.assertEqual(routine_ref.dataset_id, "dataset_1") + self.assertEqual(routine_ref.routine_id, "routine_1") + def test_to_api_repr(self): dataset = self._make_one("project_1", "dataset_1")