From c4d7b472db220c94703e266a6008634dde6f5ad5 Mon Sep 17 00:00:00 2001 From: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Date: Tue, 11 Aug 2020 10:04:04 -0700 Subject: [PATCH 01/50] test(samples): change string for utf8 translation (#48) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/python-translate/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes #46 πŸ¦• --- samples/snippets/snippets_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/snippets/snippets_test.py b/samples/snippets/snippets_test.py index 6d63759d..ab489dfa 100644 --- a/samples/snippets/snippets_test.py +++ b/samples/snippets/snippets_test.py @@ -43,7 +43,7 @@ def test_translate_text(capsys): def test_translate_utf8(capsys): - text = u'λ‚˜λŠ” νŒŒμΈμ• ν”Œμ„ μ’‹μ•„ν•œλ‹€.' + text = u'νŒŒμΈμ• ν”Œ 13개' snippets.translate_text('en', text) out, _ = capsys.readouterr() - assert u'I like pineapple' in out + assert u'13 pineapples' in out From 3e8bd490d105d5648b6f80c773f2dc60e069bc8a Mon Sep 17 00:00:00 2001 From: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Date: Thu, 27 Aug 2020 11:31:38 -0600 Subject: [PATCH 02/50] test: fix vpcsc tests (#54) --- tests/system/test_vpcsc.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/tests/system/test_vpcsc.py b/tests/system/test_vpcsc.py index 814ab875..824db401 100644 --- a/tests/system/test_vpcsc.py +++ b/tests/system/test_vpcsc.py @@ -75,13 +75,13 @@ def glossary_outside(glossary_name_outside): @vpcsc_config.skip_unless_inside_vpcsc def test_create_glossary_w_inside(client, parent_inside, glossary_inside): - client.create_glossary(parent_inside, glossary_inside) + client.create_glossary(parent=parent_inside, glossary=glossary_inside) @vpcsc_config.skip_unless_inside_vpcsc def test_create_glossary_w_outside(client, parent_outside, glossary_outside): with pytest.raises(exceptions.PermissionDenied) as exc: - client.create_glossary(parent_outside, glossary_outside) + client.create_glossary(parent=parent_outside, glossary=glossary_outside) assert exc.value.message.startswith(_VPCSC_PROHIBITED_MESSAGE) @@ -94,7 +94,7 @@ def test_list_glossaries_w_inside(client, parent_inside): @vpcsc_config.skip_unless_inside_vpcsc def test_list_glossaries_w_outside(client, parent_outside): with pytest.raises(exceptions.PermissionDenied) as exc: - list(client.list_glossaries(parent_outside)) + list(client.list_glossaries(parent=parent_outside)) assert exc.value.message.startswith(_VPCSC_PROHIBITED_MESSAGE) @@ -102,7 +102,7 @@ def test_list_glossaries_w_outside(client, parent_outside): @vpcsc_config.skip_unless_inside_vpcsc def test_get_glossary_w_inside(client, glossary_name_inside): try: - client.get_glossary(glossary_name_inside) + client.get_glossary(name=glossary_name_inside) except exceptions.NotFound: # no perms issue pass @@ -110,7 +110,7 @@ def test_get_glossary_w_inside(client, glossary_name_inside): @vpcsc_config.skip_unless_inside_vpcsc def test_get_glossary_w_outside(client, glossary_name_outside): with pytest.raises(exceptions.PermissionDenied) as exc: - client.get_glossary(glossary_name_outside) + client.get_glossary(name=glossary_name_outside) assert exc.value.message.startswith(_VPCSC_PROHIBITED_MESSAGE) @@ -118,7 +118,7 @@ def test_get_glossary_w_outside(client, glossary_name_outside): @vpcsc_config.skip_unless_inside_vpcsc def test_delete_glossary_w_inside(client, glossary_name_inside): try: - client.delete_glossary(glossary_name_inside) + client.delete_glossary(name=glossary_name_inside) except exceptions.NotFound: # no perms issue pass @@ -126,7 +126,7 @@ def test_delete_glossary_w_inside(client, glossary_name_inside): @vpcsc_config.skip_unless_inside_vpcsc def test_delete_glossary_w_outside(client, glossary_name_outside): with pytest.raises(exceptions.PermissionDenied) as exc: - client.delete_glossary(glossary_name_outside) + client.delete_glossary(name=glossary_name_outside) assert exc.value.message.startswith(_VPCSC_PROHIBITED_MESSAGE) @@ -140,11 +140,13 @@ def test_batch_translate_text_w_inside(client, parent_inside): "gcs_destination": {"output_uri_prefix": "gs://fake-bucket/output/"} } client.batch_translate_text( # no perms issue - parent_inside, - source_language_code, - target_language_codes, - input_configs, - output_config, + request={ + "parent": parent_inside, + "source_language_code": source_language_code, + "target_language_codes": target_language_codes, + "input_configs": input_configs, + "output_config": output_config, + } ) @@ -158,11 +160,13 @@ def test_batch_translate_text_w_outside(client, parent_outside): } with pytest.raises(exceptions.PermissionDenied) as exc: client.batch_translate_text( - parent_outside, - source_language_code, - target_language_codes, - input_configs, - output_config, + request={ + "parent": parent_inside, + "source_language_code": source_language_code, + "target_language_codes": target_language_codes, + "input_configs": input_configs, + "output_config": output_config, + } ) assert exc.value.message.startswith(_VPCSC_PROHIBITED_MESSAGE) From 5b8929b0f687e9bf78eecf795e005339c53a62ca Mon Sep 17 00:00:00 2001 From: Mike <45373284+munkhuushmgl@users.noreply.github.com> Date: Mon, 31 Aug 2020 16:20:48 -0700 Subject: [PATCH 03/50] samples: fixed flaky test (#57) --- samples/snippets/hybrid_glossaries/hybrid_tutorial.py | 1 + .../snippets/hybrid_glossaries/hybrid_tutorial_test.py | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/samples/snippets/hybrid_glossaries/hybrid_tutorial.py b/samples/snippets/hybrid_glossaries/hybrid_tutorial.py index c4a84345..a9ea9328 100644 --- a/samples/snippets/hybrid_glossaries/hybrid_tutorial.py +++ b/samples/snippets/hybrid_glossaries/hybrid_tutorial.py @@ -56,6 +56,7 @@ def pic_to_text(infile): # For less dense text, use text_detection response = client.document_text_detection(image=image) text = response.full_text_annotation.text + print("Detected text: {}".format(text)) return text # [END translate_hybrid_vision] diff --git a/samples/snippets/hybrid_glossaries/hybrid_tutorial_test.py b/samples/snippets/hybrid_glossaries/hybrid_tutorial_test.py index 8df7cc35..56439688 100644 --- a/samples/snippets/hybrid_glossaries/hybrid_tutorial_test.py +++ b/samples/snippets/hybrid_glossaries/hybrid_tutorial_test.py @@ -13,7 +13,6 @@ # limitations under the License. import os -import re import sys import uuid @@ -29,11 +28,13 @@ # VISION TESTS -def test_vision_standard_format(): +def test_vision_standard_format(capsys): # Generate text using Vision API - text = pic_to_text('resources/standard_format.jpeg') + pic_to_text('resources/standard_format.jpeg') + out, err = capsys.readouterr() - assert re.match(r"This\s?is\s?a\s?test!\s?", text) + assert 'Detected text:' in out + assert 'test!' in out def test_vision_non_standard_format(): From ba8eaffe8c455d52c4bd17de6986d13f01511185 Mon Sep 17 00:00:00 2001 From: Torry Yang Date: Fri, 20 Jul 2018 16:24:34 -0700 Subject: [PATCH 04/50] automl beta [(#1575)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/1575) * automl initial commit * lint * fix import groupings * add requirements.txt * address review comments --- .../snippets/automl_translation_dataset.py | 278 ++++++++++++++++ samples/snippets/automl_translation_model.py | 300 ++++++++++++++++++ .../snippets/automl_translation_predict.py | 109 +++++++ samples/snippets/dataset_test.py | 69 ++++ samples/snippets/model_test.py | 78 +++++ samples/snippets/predict_test.py | 31 ++ samples/snippets/requirements.txt | 1 + samples/snippets/resources/input.txt | 1 + 8 files changed, 867 insertions(+) create mode 100755 samples/snippets/automl_translation_dataset.py create mode 100755 samples/snippets/automl_translation_model.py create mode 100644 samples/snippets/automl_translation_predict.py create mode 100644 samples/snippets/dataset_test.py create mode 100644 samples/snippets/model_test.py create mode 100644 samples/snippets/predict_test.py create mode 100644 samples/snippets/resources/input.txt diff --git a/samples/snippets/automl_translation_dataset.py b/samples/snippets/automl_translation_dataset.py new file mode 100755 index 00000000..e579ac35 --- /dev/null +++ b/samples/snippets/automl_translation_dataset.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python + +# Copyright 2018 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 +# +# http://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. + +"""This application demonstrates how to perform basic operations on dataset +with the Google AutoML Translation API. + +For more information, see the documentation at +https://cloud.google.com/translate/automl/docs +""" + +import argparse +import os + + +def create_dataset(project_id, compute_region, dataset_name, source, target): + """Create a dataset.""" + # [START automl_translation_create_dataset] + # TODO(developer): Uncomment and set the following variables + # project_id = 'PROJECT_ID_HERE' + # compute_region = 'COMPUTE_REGION_HERE' + # dataset_name = 'DATASET_NAME_HERE' + # source = 'LANGUAGE_CODE_OF_SOURCE_LANGUAGE' + # target = 'LANGUAGE_CODE_OF_TARGET_LANGUAGE' + + from google.cloud import automl_v1beta1 as automl + + client = automl.AutoMlClient() + + # A resource that represents Google Cloud Platform location. + project_location = client.location_path(project_id, compute_region) + + # Specify the source and target language. + dataset_metadata = { + "source_language_code": source, + "target_language_code": target, + } + # Set dataset name and dataset metadata + my_dataset = { + "display_name": dataset_name, + "translation_dataset_metadata": dataset_metadata, + } + + # Create a dataset with the dataset metadata in the region. + dataset = client.create_dataset(project_location, my_dataset) + + # Display the dataset information + print("Dataset name: {}".format(dataset.name)) + print("Dataset id: {}".format(dataset.name.split("/")[-1])) + print("Dataset display name: {}".format(dataset.display_name)) + print("Translation dataset Metadata:") + print( + "\tsource_language_code: {}".format( + dataset.translation_dataset_metadata.source_language_code + ) + ) + print( + "\ttarget_language_code: {}".format( + dataset.translation_dataset_metadata.target_language_code + ) + ) + print("Dataset create time:") + print("\tseconds: {}".format(dataset.create_time.seconds)) + print("\tnanos: {}".format(dataset.create_time.nanos)) + + # [END automl_translation_create_dataset] + + +def list_datasets(project_id, compute_region, filter_): + """List Datasets.""" + # [START automl_translation_list_datasets] + # TODO(developer): Uncomment and set the following variables + # project_id = 'PROJECT_ID_HERE' + # compute_region = 'COMPUTE_REGION_HERE' + # filter_ = 'filter expression here' + + from google.cloud import automl_v1beta1 as automl + + client = automl.AutoMlClient() + + # A resource that represents Google Cloud Platform location. + project_location = client.location_path(project_id, compute_region) + + # List all the datasets available in the region by applying filter. + response = client.list_datasets(project_location, filter_) + + print("List of datasets:") + for dataset in response: + # Display the dataset information + print("Dataset name: {}".format(dataset.name)) + print("Dataset id: {}".format(dataset.name.split("/")[-1])) + print("Dataset display name: {}".format(dataset.display_name)) + print("Translation dataset metadata:") + print( + "\tsource_language_code: {}".format( + dataset.translation_dataset_metadata.source_language_code + ) + ) + print( + "\ttarget_language_code: {}".format( + dataset.translation_dataset_metadata.target_language_code + ) + ) + print("Dataset create time:") + print("\tseconds: {}".format(dataset.create_time.seconds)) + print("\tnanos: {}".format(dataset.create_time.nanos)) + + # [END automl_translation_list_datasets] + + +def get_dataset(project_id, compute_region, dataset_id): + """Get the dataset.""" + # [START automl_translation_get_dataset] + # TODO(developer): Uncomment and set the following variables + # project_id = 'PROJECT_ID_HERE' + # compute_region = 'COMPUTE_REGION_HERE' + # dataset_id = 'DATASET_ID_HERE' + + from google.cloud import automl_v1beta1 as automl + + client = automl.AutoMlClient() + + # Get the full path of the dataset + dataset_full_id = client.dataset_path( + project_id, compute_region, dataset_id + ) + + # Get complete detail of the dataset. + dataset = client.get_dataset(dataset_full_id) + + # Display the dataset information + print("Dataset name: {}".format(dataset.name)) + print("Dataset id: {}".format(dataset.name.split("/")[-1])) + print("Dataset display name: {}".format(dataset.display_name)) + print("Translation dataset metadata:") + print( + "\tsource_language_code: {}".format( + dataset.translation_dataset_metadata.source_language_code + ) + ) + print( + "\ttarget_language_code: {}".format( + dataset.translation_dataset_metadata.target_language_code + ) + ) + print("Dataset create time:") + print("\tseconds: {}".format(dataset.create_time.seconds)) + print("\tnanos: {}".format(dataset.create_time.nanos)) + + # [END automl_translation_get_dataset] + + +def import_data(project_id, compute_region, dataset_id, path): + """Import sentence pairs to the dataset.""" + # [START automl_translation_import_data] + # TODO(developer): Uncomment and set the following variables + # project_id = 'PROJECT_ID_HERE' + # compute_region = 'COMPUTE_REGION_HERE' + # dataset_id = 'DATASET_ID_HERE' + # path = 'gs://path/to/file.csv' + + from google.cloud import automl_v1beta1 as automl + + client = automl.AutoMlClient() + + # Get the full path of the dataset. + dataset_full_id = client.dataset_path( + project_id, compute_region, dataset_id + ) + + # Get the multiple Google Cloud Storage URIs + input_uris = path.split(",") + input_config = {"gcs_source": {"input_uris": input_uris}} + + # Import data from the input URI + response = client.import_data(dataset_full_id, input_config) + + print("Processing import...") + # synchronous check of operation status + print("Data imported. {}".format(response.result())) + + # [END automl_translation_import_data] + + +def delete_dataset(project_id, compute_region, dataset_id): + """Delete a dataset.""" + # [START automl_translation_delete_dataset]] + # TODO(developer): Uncomment and set the following variables + # project_id = 'PROJECT_ID_HERE' + # compute_region = 'COMPUTE_REGION_HERE' + # dataset_id = 'DATASET_ID_HERE' + + from google.cloud import automl_v1beta1 as automl + + client = automl.AutoMlClient() + + # Get the full path of the dataset. + dataset_full_id = client.dataset_path( + project_id, compute_region, dataset_id + ) + + # Delete a dataset. + response = client.delete_dataset(dataset_full_id) + + # synchronous check of operation status + print("Dataset deleted. {}".format(response.result())) + + # [END automl_translation_delete_dataset] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + subparsers = parser.add_subparsers(dest="command") + + create_dataset_parser = subparsers.add_parser( + "create_dataset", help=create_dataset.__doc__ + ) + create_dataset_parser.add_argument("dataset_name") + create_dataset_parser.add_argument("source") + create_dataset_parser.add_argument("target") + + list_datasets_parser = subparsers.add_parser( + "list_datasets", help=list_datasets.__doc__ + ) + list_datasets_parser.add_argument("filter", nargs="?", default="") + + import_data_parser = subparsers.add_parser( + "import_data", help=import_data.__doc__ + ) + import_data_parser.add_argument("dataset_id") + import_data_parser.add_argument("path") + + delete_dataset_parser = subparsers.add_parser( + "delete_dataset", help=delete_dataset.__doc__ + ) + delete_dataset_parser.add_argument("dataset_id") + + get_dataset_parser = subparsers.add_parser( + "get_dataset", help=get_dataset.__doc__ + ) + get_dataset_parser.add_argument("dataset_id") + + project_id = os.environ["PROJECT_ID"] + compute_region = os.environ["REGION_NAME"] + + args = parser.parse_args() + + if args.command == "create_dataset": + create_dataset( + project_id, + compute_region, + args.dataset_name, + args.source, + args.target, + ) + if args.command == "list_datasets": + list_datasets(project_id, compute_region, args.filter) + if args.command == "get_dataset": + get_dataset(project_id, compute_region, args.dataset_id) + if args.command == "import_data": + import_data(project_id, compute_region, args.dataset_id, args.path) + if args.command == "delete_dataset": + delete_dataset(project_id, compute_region, args.dataset_id) diff --git a/samples/snippets/automl_translation_model.py b/samples/snippets/automl_translation_model.py new file mode 100755 index 00000000..0b9b6f53 --- /dev/null +++ b/samples/snippets/automl_translation_model.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python + +# Copyright 2018 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 +# +# http://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. + +"""This application demonstrates how to perform basic operations on model +with the Google AutoML Translation API. + +For more information, see the documentation at +https://cloud.google.com/translate/automl/docs +""" + +import argparse +import os + + +def create_model(project_id, compute_region, dataset_id, model_name): + """Create a model.""" + # [START automl_translation_create_model] + # TODO(developer): Uncomment and set the following variables + # project_id = 'PROJECT_ID_HERE' + # compute_region = 'COMPUTE_REGION_HERE' + # dataset_id = 'DATASET_ID_HERE' + # model_name = 'MODEL_NAME_HERE' + + from google.cloud import automl_v1beta1 as automl + + client = automl.AutoMlClient() + + # A resource that represents Google Cloud Platform location. + project_location = client.location_path(project_id, compute_region) + + # Set model name and dataset. + my_model = { + "display_name": model_name, + "dataset_id": dataset_id, + "translation_model_metadata": {"base_model": ""}, + } + + # Create a model with the model metadata in the region. + response = client.create_model(project_location, my_model) + + print("Training operation name: {}".format(response.operation.name)) + print("Training started...") + + # [END automl_translation_create_model] + + +def list_models(project_id, compute_region, filter_): + """List all models.""" + # [START automl_translation_list_models] + # TODO(developer): Uncomment and set the following variables + # project_id = 'PROJECT_ID_HERE' + # compute_region = 'COMPUTE_REGION_HERE' + # filter_ = 'DATASET_ID_HERE' + + from google.cloud import automl_v1beta1 as automl + from google.cloud.automl_v1beta1 import enums + + client = automl.AutoMlClient() + + # A resource that represents Google Cloud Platform location. + project_location = client.location_path(project_id, compute_region) + + # List all the models available in the region by applying filter. + response = client.list_models(project_location, filter_) + + print("List of models:") + for model in response: + # Display the model information. + if model.deployment_state == enums.Model.DeploymentState.DEPLOYED: + deployment_state = "deployed" + else: + deployment_state = "undeployed" + + print("Model name: {}".format(model.name)) + print("Model id: {}".format(model.name.split("/")[-1])) + print("Model display name: {}".format(model.display_name)) + print("Model create time:") + print("\tseconds: {}".format(model.create_time.seconds)) + print("\tnanos: {}".format(model.create_time.nanos)) + print("Model deployment state: {}".format(deployment_state)) + + # [END automl_translation_list_models] + + +def get_model(project_id, compute_region, model_id): + """Get model details.""" + # [START automl_translation_get_model] + # TODO(developer): Uncomment and set the following variables + # project_id = 'PROJECT_ID_HERE' + # compute_region = 'COMPUTE_REGION_HERE' + # model_id = 'MODEL_ID_HERE' + + from google.cloud import automl_v1beta1 as automl + from google.cloud.automl_v1beta1 import enums + + client = automl.AutoMlClient() + + # Get the full path of the model. + model_full_id = client.model_path(project_id, compute_region, model_id) + + # Get complete detail of the model. + model = client.get_model(model_full_id) + + # Retrieve deployment state. + if model.deployment_state == enums.Model.DeploymentState.DEPLOYED: + deployment_state = "deployed" + else: + deployment_state = "undeployed" + + # Display the model information. + print("Model name: {}".format(model.name)) + print("Model id: {}".format(model.name.split("/")[-1])) + print("Model display name: {}".format(model.display_name)) + print("Model create time:") + print("\tseconds: {}".format(model.create_time.seconds)) + print("\tnanos: {}".format(model.create_time.nanos)) + print("Model deployment state: {}".format(deployment_state)) + + # [END automl_translation_get_model] + + +def list_model_evaluations(project_id, compute_region, model_id, filter_): + """List model evaluations.""" + # [START automl_translation_list_model_evaluations] + # TODO(developer): Uncomment and set the following variables + # project_id = 'PROJECT_ID_HERE' + # compute_region = 'COMPUTE_REGION_HERE' + # model_id = 'MODEL_ID_HERE' + # filter_ = 'filter expression here' + + from google.cloud import automl_v1beta1 as automl + + client = automl.AutoMlClient() + + # Get the full path of the model. + model_full_id = client.model_path(project_id, compute_region, model_id) + + print("List of model evaluations:") + for element in client.list_model_evaluations(model_full_id, filter_): + print(element) + + # [END automl_translation_list_model_evaluations] + + +def get_model_evaluation( + project_id, compute_region, model_id, model_evaluation_id +): + """Get model evaluation.""" + # [START automl_translation_get_model_evaluation] + # TODO(developer): Uncomment and set the following variables + # project_id = 'PROJECT_ID_HERE' + # compute_region = 'COMPUTE_REGION_HERE' + # model_id = 'MODEL_ID_HERE' + # model_evaluation_id = 'MODEL_EVALUATION_ID_HERE' + + from google.cloud import automl_v1beta1 as automl + + client = automl.AutoMlClient() + + # Get the full path of the model evaluation. + model_evaluation_full_id = client.model_evaluation_path( + project_id, compute_region, model_id, model_evaluation_id + ) + + # Get complete detail of the model evaluation. + response = client.get_model_evaluation(model_evaluation_full_id) + + print(response) + + # [END automl_translation_get_model_evaluation] + + +def delete_model(project_id, compute_region, model_id): + """Delete a model.""" + # [START automl_translation_delete_model] + # TODO(developer): Uncomment and set the following variables + # project_id = 'PROJECT_ID_HERE' + # compute_region = 'COMPUTE_REGION_HERE' + # model_id = 'MODEL_ID_HERE' + + from google.cloud import automl_v1beta1 as automl + + client = automl.AutoMlClient() + + # Get the full path of the model. + model_full_id = client.model_path(project_id, compute_region, model_id) + + # Delete a model. + response = client.delete_model(model_full_id) + + # synchronous check of operation status. + print("Model deleted. {}".format(response.result())) + + # [END automl_translation_delete_model] + + +def get_operation_status(operation_full_id): + """Get operation status.""" + # [START automl_translation_get_operation_status] + # TODO(developer): Uncomment and set the following variables + # operation_full_id = + # 'projects//locations//operations/' + + from google.cloud import automl_v1beta1 as automl + + client = automl.AutoMlClient() + + # Get the latest state of a long-running operation. + response = client.transport._operations_client.get_operation( + operation_full_id + ) + + print("Operation status: {}".format(response)) + + # [END automl_translation_get_operation_status] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + subparsers = parser.add_subparsers(dest="command") + + create_model_parser = subparsers.add_parser( + "create_model", help=create_model.__doc__ + ) + create_model_parser.add_argument("dataset_id") + create_model_parser.add_argument("model_name") + + list_model_evaluations_parser = subparsers.add_parser( + "list_model_evaluations", help=list_model_evaluations.__doc__ + ) + list_model_evaluations_parser.add_argument("model_id") + list_model_evaluations_parser.add_argument("filter", nargs="?", default="") + + get_model_evaluation_parser = subparsers.add_parser( + "get_model_evaluation", help=get_model_evaluation.__doc__ + ) + get_model_evaluation_parser.add_argument("model_id") + get_model_evaluation_parser.add_argument("model_evaluation_id") + + get_model_parser = subparsers.add_parser( + "get_model", help=get_model.__doc__ + ) + get_model_parser.add_argument("model_id") + + get_operation_status_parser = subparsers.add_parser( + "get_operation_status", help=get_operation_status.__doc__ + ) + get_operation_status_parser.add_argument("operation_full_id") + + list_models_parser = subparsers.add_parser( + "list_models", help=list_models.__doc__ + ) + list_models_parser.add_argument("filter", nargs="?", default="") + + delete_model_parser = subparsers.add_parser( + "delete_model", help=delete_model.__doc__ + ) + delete_model_parser.add_argument("model_id") + + project_id = os.environ["PROJECT_ID"] + compute_region = os.environ["REGION_NAME"] + + args = parser.parse_args() + + if args.command == "create_model": + create_model( + project_id, compute_region, args.dataset_id, args.model_name + ) + if args.command == "list_models": + list_models(project_id, compute_region, args.filter) + if args.command == "get_model": + get_model(project_id, compute_region, args.model_id) + if args.command == "list_model_evaluations": + list_model_evaluations( + project_id, compute_region, args.model_id, args.filter + ) + if args.command == "get_model_evaluation": + get_model_evaluation( + project_id, compute_region, args.model_id, args.model_evaluation_id + ) + if args.command == "delete_model": + delete_model(project_id, compute_region, args.model_id) + if args.command == "get_operation_status": + get_operation_status(args.operation_full_id) diff --git a/samples/snippets/automl_translation_predict.py b/samples/snippets/automl_translation_predict.py new file mode 100644 index 00000000..1dac70b7 --- /dev/null +++ b/samples/snippets/automl_translation_predict.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python + +# Copyright 2018 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 +# +# http://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. + +"""This application demonstrates how to perform basic operations on prediction +with the Google AutoML Translation API. + +For more information, see the documentation at +https://cloud.google.com/translate/automl/docs +""" + +import argparse +import os + + +def predict( + project_id, + compute_region, + model_id, + file_path, + translation_allow_fallback=False, +): + """Translate the content.""" + # [START automl_translation_predict] + # project_id = 'PROJECT_ID_HERE' + # compute_region = 'COMPUTE_REGION_HERE' + # model_id = 'MODEL_ID_HERE' + # file_path = '/local/path/to/file' + # translation_allow_fallback = True allows fallback to Google Translate + + from google.cloud import automl_v1beta1 as automl + + automl_client = automl.AutoMlClient() + + # Create client for prediction service. + prediction_client = automl.PredictionServiceClient() + + # Get the full path of the model. + model_full_id = automl_client.model_path( + project_id, compute_region, model_id + ) + + # Read the file content for translation. + with open(file_path, "rb") as content_file: + content = content_file.read() + content.decode("utf-8") + + # Set the payload by giving the content of the file. + payload = {"text_snippet": {"content": content}} + + # params is additional domain-specific parameters. + # translation_allow_fallback allows to use Google translation model. + params = {} + if translation_allow_fallback: + params = {"translation_allow_fallback": "True"} + + response = prediction_client.predict(model_full_id, payload, params) + translated_content = response.payload[0].translation.translated_content + + print(u"Translated content: {}".format(translated_content.content)) + + # [END automl_translation_predict] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + subparsers = parser.add_subparsers(dest="command") + + predict_parser = subparsers.add_parser("predict", help=predict.__doc__) + predict_parser.add_argument("model_id") + predict_parser.add_argument("file_path") + predict_parser.add_argument( + "translation_allow_fallback", + nargs="?", + choices=["False", "True"], + default="False", + ) + + project_id = os.environ["PROJECT_ID"] + compute_region = os.environ["REGION_NAME"] + + args = parser.parse_args() + + if args.command == "predict": + translation_allow_fallback = ( + True if args.translation_allow_fallback == "True" else False + ) + predict( + project_id, + compute_region, + args.model_id, + args.file_path, + translation_allow_fallback, + ) diff --git a/samples/snippets/dataset_test.py b/samples/snippets/dataset_test.py new file mode 100644 index 00000000..29e3e5c9 --- /dev/null +++ b/samples/snippets/dataset_test.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +# Copyright 2018 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 +# +# http://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 os + +import pytest + +import automl_translation_dataset + +project_id = os.environ["GCLOUD_PROJECT"] +compute_region = "us-central1" + + +@pytest.mark.slow +def test_dataset_create_import_delete(capsys): + # create dataset + dataset_name = "test_" + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + automl_translation_dataset.create_dataset( + project_id, compute_region, dataset_name, "en", "ja" + ) + out, _ = capsys.readouterr() + create_dataset_output = out.splitlines() + assert "Dataset id: " in create_dataset_output[1] + + # import data + dataset_id = create_dataset_output[1].split()[2] + data = "gs://{}-vcm/en-ja.csv".format(project_id) + automl_translation_dataset.import_data( + project_id, compute_region, dataset_id, data + ) + out, _ = capsys.readouterr() + assert "Data imported." in out + + # delete dataset + automl_translation_dataset.delete_dataset( + project_id, compute_region, dataset_id + ) + out, _ = capsys.readouterr() + assert "Dataset deleted." in out + + +def test_dataset_list_get(capsys): + # list datasets + automl_translation_dataset.list_datasets(project_id, compute_region, "") + out, _ = capsys.readouterr() + list_dataset_output = out.splitlines() + assert "Dataset id: " in list_dataset_output[2] + + # get dataset + dataset_id = list_dataset_output[2].split()[2] + automl_translation_dataset.get_dataset( + project_id, compute_region, dataset_id + ) + out, _ = capsys.readouterr() + assert "Dataset name: " in out diff --git a/samples/snippets/model_test.py b/samples/snippets/model_test.py new file mode 100644 index 00000000..7f915c5d --- /dev/null +++ b/samples/snippets/model_test.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +# Copyright 2018 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 +# +# http://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 os + +from google.cloud import automl_v1beta1 as automl + +import automl_translation_model + +project_id = os.environ["GCLOUD_PROJECT"] +compute_region = "us-central1" + + +def test_model_create_status_delete(capsys): + # create model + client = automl.AutoMlClient() + model_name = "test_" + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + project_location = client.location_path(project_id, compute_region) + my_model = { + "display_name": model_name, + "dataset_id": "3876092572857648864", + "translation_model_metadata": {"base_model": ""}, + } + response = client.create_model(project_location, my_model) + operation_name = response.operation.name + assert operation_name + + # get operation status + automl_translation_model.get_operation_status(operation_name) + out, _ = capsys.readouterr() + assert "Operation status: " in out + + # cancel operation + response.cancel() + + +def test_model_list_get_evaluate(capsys): + # list models + automl_translation_model.list_models(project_id, compute_region, "") + out, _ = capsys.readouterr() + list_models_output = out.splitlines() + assert "Model id: " in list_models_output[2] + + # get model + model_id = list_models_output[2].split()[2] + automl_translation_model.get_model(project_id, compute_region, model_id) + out, _ = capsys.readouterr() + assert "Model name: " in out + + # list model evaluations + automl_translation_model.list_model_evaluations( + project_id, compute_region, model_id, "" + ) + out, _ = capsys.readouterr() + list_evals_output = out.splitlines() + assert "name: " in list_evals_output[1] + + # get model evaluation + model_evaluation_id = list_evals_output[1].split("/")[-1][:-1] + automl_translation_model.get_model_evaluation( + project_id, compute_region, model_id, model_evaluation_id + ) + out, _ = capsys.readouterr() + assert "evaluation_metric" in out diff --git a/samples/snippets/predict_test.py b/samples/snippets/predict_test.py new file mode 100644 index 00000000..87aea8fa --- /dev/null +++ b/samples/snippets/predict_test.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +# Copyright 2018 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 +# +# http://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 os + +import automl_translation_predict + +project_id = os.environ["GCLOUD_PROJECT"] +compute_region = "us-central1" + + +def test_predict(capsys): + model_id = "3128559826197068699" + automl_translation_predict.predict( + project_id, compute_region, model_id, "resources/input.txt", False + ) + out, _ = capsys.readouterr() + assert "Translated content: " in out diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 4c26010b..9960cc67 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,2 +1,3 @@ google-cloud-translate==3.0.0 google-cloud-storage==1.30.0 +google-cloud-automl==0.1.0 diff --git a/samples/snippets/resources/input.txt b/samples/snippets/resources/input.txt new file mode 100644 index 00000000..5aecd659 --- /dev/null +++ b/samples/snippets/resources/input.txt @@ -0,0 +1 @@ +Tell me how this ends \ No newline at end of file From f7228f43f7ce6665bd7a1983f9a00a4c95eb4731 Mon Sep 17 00:00:00 2001 From: Torry Yang Date: Tue, 24 Jul 2018 09:19:56 -0700 Subject: [PATCH 05/50] remove translate prediction fallback [(#1598)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/1598) --- .../snippets/automl_translation_predict.py | 29 ++----------------- samples/snippets/predict_test.py | 2 +- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/samples/snippets/automl_translation_predict.py b/samples/snippets/automl_translation_predict.py index 1dac70b7..653cf388 100644 --- a/samples/snippets/automl_translation_predict.py +++ b/samples/snippets/automl_translation_predict.py @@ -25,20 +25,13 @@ import os -def predict( - project_id, - compute_region, - model_id, - file_path, - translation_allow_fallback=False, -): +def predict(project_id, compute_region, model_id, file_path): """Translate the content.""" # [START automl_translation_predict] # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' # model_id = 'MODEL_ID_HERE' # file_path = '/local/path/to/file' - # translation_allow_fallback = True allows fallback to Google Translate from google.cloud import automl_v1beta1 as automl @@ -61,10 +54,7 @@ def predict( payload = {"text_snippet": {"content": content}} # params is additional domain-specific parameters. - # translation_allow_fallback allows to use Google translation model. params = {} - if translation_allow_fallback: - params = {"translation_allow_fallback": "True"} response = prediction_client.predict(model_full_id, payload, params) translated_content = response.payload[0].translation.translated_content @@ -84,12 +74,6 @@ def predict( predict_parser = subparsers.add_parser("predict", help=predict.__doc__) predict_parser.add_argument("model_id") predict_parser.add_argument("file_path") - predict_parser.add_argument( - "translation_allow_fallback", - nargs="?", - choices=["False", "True"], - default="False", - ) project_id = os.environ["PROJECT_ID"] compute_region = os.environ["REGION_NAME"] @@ -97,13 +81,4 @@ def predict( args = parser.parse_args() if args.command == "predict": - translation_allow_fallback = ( - True if args.translation_allow_fallback == "True" else False - ) - predict( - project_id, - compute_region, - args.model_id, - args.file_path, - translation_allow_fallback, - ) + predict(project_id, compute_region, args.model_id, args.file_path) diff --git a/samples/snippets/predict_test.py b/samples/snippets/predict_test.py index 87aea8fa..c9fb7e04 100644 --- a/samples/snippets/predict_test.py +++ b/samples/snippets/predict_test.py @@ -25,7 +25,7 @@ def test_predict(capsys): model_id = "3128559826197068699" automl_translation_predict.predict( - project_id, compute_region, model_id, "resources/input.txt", False + project_id, compute_region, model_id, "resources/input.txt" ) out, _ = capsys.readouterr() assert "Translated content: " in out From e4a9c20e498acf41f972fc2068665446cce71577 Mon Sep 17 00:00:00 2001 From: Torry Yang Date: Thu, 2 Aug 2018 17:40:16 -0700 Subject: [PATCH 06/50] skip automl model create/delete test [(#1608)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/1608) * skip model create/delete test * add skip reason --- samples/snippets/model_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/samples/snippets/model_test.py b/samples/snippets/model_test.py index 7f915c5d..0d37a85c 100644 --- a/samples/snippets/model_test.py +++ b/samples/snippets/model_test.py @@ -18,6 +18,7 @@ import os from google.cloud import automl_v1beta1 as automl +import pytest import automl_translation_model @@ -25,6 +26,7 @@ compute_region = "us-central1" +@pytest.mark.skip(reason="creates too many models") def test_model_create_status_delete(capsys): # create model client = automl.AutoMlClient() From 2eebf4523de21528d4b84b01268507490213dde7 Mon Sep 17 00:00:00 2001 From: DPE bot Date: Tue, 28 Aug 2018 11:17:45 -0700 Subject: [PATCH 07/50] Auto-update dependencies. [(#1658)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/1658) * Auto-update dependencies. * Rollback appengine/standard/bigquery/. * Rollback appengine/standard/iap/. * Rollback bigtable/metricscaler. * Rolledback appengine/flexible/datastore. * Rollback dataproc/ * Rollback jobs/api_client * Rollback vision/cloud-client. * Rollback functions/ocr/app. * Rollback iot/api-client/end_to_end_example. * Rollback storage/cloud-client. * Rollback kms/api-client. * Rollback dlp/ * Rollback bigquery/cloud-client. * Rollback iot/api-client/manager. * Rollback appengine/flexible/cloudsql_postgresql. --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 9960cc67..b8869603 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,3 @@ google-cloud-translate==3.0.0 google-cloud-storage==1.30.0 -google-cloud-automl==0.1.0 +google-cloud-automl==0.1.1 From 95aa2d05f070451b9e9016900db48420d66e01f6 Mon Sep 17 00:00:00 2001 From: Alix Hamilton Date: Wed, 29 Aug 2018 12:37:06 -0700 Subject: [PATCH 08/50] Update AutoML region tags to use standard product prefixes [(#1669)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/1669) --- .../snippets/automl_translation_dataset.py | 20 ++++++------- samples/snippets/automl_translation_model.py | 28 +++++++++---------- .../snippets/automl_translation_predict.py | 4 +-- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/samples/snippets/automl_translation_dataset.py b/samples/snippets/automl_translation_dataset.py index e579ac35..c60ef544 100755 --- a/samples/snippets/automl_translation_dataset.py +++ b/samples/snippets/automl_translation_dataset.py @@ -27,7 +27,7 @@ def create_dataset(project_id, compute_region, dataset_name, source, target): """Create a dataset.""" - # [START automl_translation_create_dataset] + # [START automl_translate_create_dataset] # TODO(developer): Uncomment and set the following variables # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' @@ -75,12 +75,12 @@ def create_dataset(project_id, compute_region, dataset_name, source, target): print("\tseconds: {}".format(dataset.create_time.seconds)) print("\tnanos: {}".format(dataset.create_time.nanos)) - # [END automl_translation_create_dataset] + # [END automl_translate_create_dataset] def list_datasets(project_id, compute_region, filter_): """List Datasets.""" - # [START automl_translation_list_datasets] + # [START automl_translate_list_datasets] # TODO(developer): Uncomment and set the following variables # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' @@ -117,12 +117,12 @@ def list_datasets(project_id, compute_region, filter_): print("\tseconds: {}".format(dataset.create_time.seconds)) print("\tnanos: {}".format(dataset.create_time.nanos)) - # [END automl_translation_list_datasets] + # [END automl_translate_list_datasets] def get_dataset(project_id, compute_region, dataset_id): """Get the dataset.""" - # [START automl_translation_get_dataset] + # [START automl_translate_get_dataset] # TODO(developer): Uncomment and set the following variables # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' @@ -159,12 +159,12 @@ def get_dataset(project_id, compute_region, dataset_id): print("\tseconds: {}".format(dataset.create_time.seconds)) print("\tnanos: {}".format(dataset.create_time.nanos)) - # [END automl_translation_get_dataset] + # [END automl_translate_get_dataset] def import_data(project_id, compute_region, dataset_id, path): """Import sentence pairs to the dataset.""" - # [START automl_translation_import_data] + # [START automl_translate_import_data] # TODO(developer): Uncomment and set the following variables # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' @@ -191,12 +191,12 @@ def import_data(project_id, compute_region, dataset_id, path): # synchronous check of operation status print("Data imported. {}".format(response.result())) - # [END automl_translation_import_data] + # [END automl_translate_import_data] def delete_dataset(project_id, compute_region, dataset_id): """Delete a dataset.""" - # [START automl_translation_delete_dataset]] + # [START automl_translate_delete_dataset]] # TODO(developer): Uncomment and set the following variables # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' @@ -217,7 +217,7 @@ def delete_dataset(project_id, compute_region, dataset_id): # synchronous check of operation status print("Dataset deleted. {}".format(response.result())) - # [END automl_translation_delete_dataset] + # [END automl_translate_delete_dataset] if __name__ == "__main__": diff --git a/samples/snippets/automl_translation_model.py b/samples/snippets/automl_translation_model.py index 0b9b6f53..77a4ed73 100755 --- a/samples/snippets/automl_translation_model.py +++ b/samples/snippets/automl_translation_model.py @@ -27,7 +27,7 @@ def create_model(project_id, compute_region, dataset_id, model_name): """Create a model.""" - # [START automl_translation_create_model] + # [START automl_translate_create_model] # TODO(developer): Uncomment and set the following variables # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' @@ -54,12 +54,12 @@ def create_model(project_id, compute_region, dataset_id, model_name): print("Training operation name: {}".format(response.operation.name)) print("Training started...") - # [END automl_translation_create_model] + # [END automl_translate_create_model] def list_models(project_id, compute_region, filter_): """List all models.""" - # [START automl_translation_list_models] + # [START automl_translate_list_models] # TODO(developer): Uncomment and set the following variables # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' @@ -92,12 +92,12 @@ def list_models(project_id, compute_region, filter_): print("\tnanos: {}".format(model.create_time.nanos)) print("Model deployment state: {}".format(deployment_state)) - # [END automl_translation_list_models] + # [END automl_translate_list_models] def get_model(project_id, compute_region, model_id): """Get model details.""" - # [START automl_translation_get_model] + # [START automl_translate_get_model] # TODO(developer): Uncomment and set the following variables # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' @@ -129,12 +129,12 @@ def get_model(project_id, compute_region, model_id): print("\tnanos: {}".format(model.create_time.nanos)) print("Model deployment state: {}".format(deployment_state)) - # [END automl_translation_get_model] + # [END automl_translate_get_model] def list_model_evaluations(project_id, compute_region, model_id, filter_): """List model evaluations.""" - # [START automl_translation_list_model_evaluations] + # [START automl_translate_list_model_evaluations] # TODO(developer): Uncomment and set the following variables # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' @@ -152,14 +152,14 @@ def list_model_evaluations(project_id, compute_region, model_id, filter_): for element in client.list_model_evaluations(model_full_id, filter_): print(element) - # [END automl_translation_list_model_evaluations] + # [END automl_translate_list_model_evaluations] def get_model_evaluation( project_id, compute_region, model_id, model_evaluation_id ): """Get model evaluation.""" - # [START automl_translation_get_model_evaluation] + # [START automl_translate_get_model_evaluation] # TODO(developer): Uncomment and set the following variables # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' @@ -180,12 +180,12 @@ def get_model_evaluation( print(response) - # [END automl_translation_get_model_evaluation] + # [END automl_translate_get_model_evaluation] def delete_model(project_id, compute_region, model_id): """Delete a model.""" - # [START automl_translation_delete_model] + # [START automl_translate_delete_model] # TODO(developer): Uncomment and set the following variables # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' @@ -204,12 +204,12 @@ def delete_model(project_id, compute_region, model_id): # synchronous check of operation status. print("Model deleted. {}".format(response.result())) - # [END automl_translation_delete_model] + # [END automl_translate_delete_model] def get_operation_status(operation_full_id): """Get operation status.""" - # [START automl_translation_get_operation_status] + # [START automl_translate_get_operation_status] # TODO(developer): Uncomment and set the following variables # operation_full_id = # 'projects//locations//operations/' @@ -225,7 +225,7 @@ def get_operation_status(operation_full_id): print("Operation status: {}".format(response)) - # [END automl_translation_get_operation_status] + # [END automl_translate_get_operation_status] if __name__ == "__main__": diff --git a/samples/snippets/automl_translation_predict.py b/samples/snippets/automl_translation_predict.py index 653cf388..b15e0e30 100644 --- a/samples/snippets/automl_translation_predict.py +++ b/samples/snippets/automl_translation_predict.py @@ -27,7 +27,7 @@ def predict(project_id, compute_region, model_id, file_path): """Translate the content.""" - # [START automl_translation_predict] + # [START automl_translate_predict] # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' # model_id = 'MODEL_ID_HERE' @@ -61,7 +61,7 @@ def predict(project_id, compute_region, model_id, file_path): print(u"Translated content: {}".format(translated_content.content)) - # [END automl_translation_predict] + # [END automl_translate_predict] if __name__ == "__main__": From 8e90ea98a6f25fe4e226b3e2d3d4a587829d034d Mon Sep 17 00:00:00 2001 From: Alix Hamilton Date: Thu, 6 Sep 2018 10:54:34 -0700 Subject: [PATCH 09/50] Fix AutoML region tag typos [(#1687)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/1687) * fixes vision delete dataset region tag * removes extra bracket --- samples/snippets/automl_translation_dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/automl_translation_dataset.py b/samples/snippets/automl_translation_dataset.py index c60ef544..cf3e50ae 100755 --- a/samples/snippets/automl_translation_dataset.py +++ b/samples/snippets/automl_translation_dataset.py @@ -196,7 +196,7 @@ def import_data(project_id, compute_region, dataset_id, path): def delete_dataset(project_id, compute_region, dataset_id): """Delete a dataset.""" - # [START automl_translate_delete_dataset]] + # [START automl_translate_delete_dataset] # TODO(developer): Uncomment and set the following variables # project_id = 'PROJECT_ID_HERE' # compute_region = 'COMPUTE_REGION_HERE' From a42f9d0c6abd97e332f3a97caa0c1045f9ee8af1 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 19 Oct 2018 15:21:41 -0700 Subject: [PATCH 10/50] Fixed name of model [(#1779)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/1779) * Fixed name of model * update model ids --- samples/snippets/predict_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/predict_test.py b/samples/snippets/predict_test.py index c9fb7e04..f9d98dfb 100644 --- a/samples/snippets/predict_test.py +++ b/samples/snippets/predict_test.py @@ -23,7 +23,7 @@ def test_predict(capsys): - model_id = "3128559826197068699" + model_id = "TRL3128559826197068699" automl_translation_predict.predict( project_id, compute_region, model_id, "resources/input.txt" ) From fad1b19b76c7a88b6a370d330e3b4a411bae2a77 Mon Sep 17 00:00:00 2001 From: DPEBot Date: Wed, 6 Feb 2019 12:06:35 -0800 Subject: [PATCH 11/50] Auto-update dependencies. [(#1980)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/1980) * Auto-update dependencies. * Update requirements.txt * Update requirements.txt --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index b8869603..11cadd58 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,3 @@ google-cloud-translate==3.0.0 google-cloud-storage==1.30.0 -google-cloud-automl==0.1.1 +google-cloud-automl==0.1.2 From cb315c820ac8caad0a3e69ce4a458fe28b20bd5b Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 26 Apr 2019 14:44:38 -0700 Subject: [PATCH 12/50] Updated beta version of automl [(#2124)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/2124) --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 11cadd58..ca4e6935 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,3 @@ google-cloud-translate==3.0.0 google-cloud-storage==1.30.0 -google-cloud-automl==0.1.2 +google-cloud-automl==1.0.1 From 2dd47addb6eeac28cad61cd49ae03f58a910490c Mon Sep 17 00:00:00 2001 From: Gus Class Date: Mon, 7 Oct 2019 15:45:22 -0700 Subject: [PATCH 13/50] Adds updates for samples profiler ... vision [(#2439)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/2439) --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index ca4e6935..8142930f 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,3 @@ google-cloud-translate==3.0.0 google-cloud-storage==1.30.0 -google-cloud-automl==1.0.1 +google-cloud-automl==1.0.1 \ No newline at end of file From 15edf6f01a1a3d4140a9176776ea1315cac353b9 Mon Sep 17 00:00:00 2001 From: DPEBot Date: Fri, 20 Dec 2019 17:41:38 -0800 Subject: [PATCH 14/50] Auto-update dependencies. [(#2005)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/2005) * Auto-update dependencies. * Revert update of appengine/flexible/datastore. * revert update of appengine/flexible/scipy * revert update of bigquery/bqml * revert update of bigquery/cloud-client * revert update of bigquery/datalab-migration * revert update of bigtable/quickstart * revert update of compute/api * revert update of container_registry/container_analysis * revert update of dataflow/run_template * revert update of datastore/cloud-ndb * revert update of dialogflow/cloud-client * revert update of dlp * revert update of functions/imagemagick * revert update of functions/ocr/app * revert update of healthcare/api-client/fhir * revert update of iam/api-client * revert update of iot/api-client/gcs_file_to_device * revert update of iot/api-client/mqtt_example * revert update of language/automl * revert update of run/image-processing * revert update of vision/automl * revert update testing/requirements.txt * revert update of vision/cloud-client/detect * revert update of vision/cloud-client/product_search * revert update of jobs/v2/api_client * revert update of jobs/v3/api_client * revert update of opencensus * revert update of translate/cloud-client * revert update to speech/cloud-client Co-authored-by: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> Co-authored-by: Doug Mahugh --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 8142930f..ca4e6935 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,3 @@ google-cloud-translate==3.0.0 google-cloud-storage==1.30.0 -google-cloud-automl==1.0.1 \ No newline at end of file +google-cloud-automl==1.0.1 From e5ffedc35f6eac33acf66228237f084692398e25 Mon Sep 17 00:00:00 2001 From: Mike <45373284+munkhuushmgl@users.noreply.github.com> Date: Mon, 2 Mar 2020 15:29:23 -0800 Subject: [PATCH 15/50] Translate: migrate published v3 translate batch samples [(#2914)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/2914) * Translate: migrate published b v3 tch samples * added missing requirements * extended wait time * inlined some vals and specified input and output * added link to supported file types & modified default values of input uri * fixed small nit --- samples/snippets/requirements.txt | 6 ++ ...late_v3_batch_translate_text_with_model.py | 67 +++++++++++++++++++ ...v3_batch_translate_text_with_model_test.py | 46 +++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 samples/snippets/translate_v3_batch_translate_text_with_model.py create mode 100644 samples/snippets/translate_v3_batch_translate_text_with_model_test.py diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index ca4e6935..8e10deaa 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,9 @@ +<<<<<<< HEAD google-cloud-translate==3.0.0 google-cloud-storage==1.30.0 google-cloud-automl==1.0.1 +======= +google-cloud-translate==2.0.0 +google-cloud-storage==1.19.1 +google-cloud-automl==0.9.0 +>>>>>>> 65dd67b5 (Translate: migrate published v3 translate batch samples [(#2914)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/2914)) diff --git a/samples/snippets/translate_v3_batch_translate_text_with_model.py b/samples/snippets/translate_v3_batch_translate_text_with_model.py new file mode 100644 index 00000000..010f7f93 --- /dev/null +++ b/samples/snippets/translate_v3_batch_translate_text_with_model.py @@ -0,0 +1,67 @@ +# Copyright 2020 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 +# +# http://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. + + +# [START translate_v3_batch_translate_text_with_model] +from google.cloud import translate + + +def batch_translate_text_with_model( + input_uri="gs://YOUR_BUCKET_ID/path/to/your/file.txt", + output_uri="gs://YOUR_BUCKET_ID/path/to/save/results/", + project_id="YOUR_PROJECT_ID", + model_id="YOUR_MODEL_ID", +): + """Batch translate text using Translation model. + Model can be AutoML or General[built-in] model. """ + + client = translate.TranslationServiceClient() + + # Supported file types: https://cloud.google.com/translate/docs/supported-formats + gcs_source = {"input_uri": input_uri} + location = "us-central1" + + input_configs_element = { + "gcs_source": gcs_source, + "mime_type": "text/plain" # Can be "text/plain" or "text/html". + } + gcs_destination = {"output_uri_prefix": output_uri} + output_config = {"gcs_destination": gcs_destination} + parent = client.location_path(project_id, location) + + model_path = "projects/{}/locations/{}/models/{}".format( + project_id, location, model_id # The location of AutoML model. + ) + + # Supported language codes: https://cloud.google.com/translate/docs/languages + models = {"ja": model_path} # takes a target lang as key. + + operation = client.batch_translate_text( + parent=parent, + source_language_code="en", + target_language_codes=["ja"], # Up to 10 language codes here. + input_configs=[input_configs_element], + output_config=output_config, + models=models, + ) + + print(u"Waiting for operation to complete...") + response = operation.result() + + # Display the translation for each input text provided. + print(u"Total Characters: {}".format(response.total_characters)) + print(u"Translated Characters: {}".format(response.translated_characters)) + + +# [END translate_v3_batch_translate_text_with_model] diff --git a/samples/snippets/translate_v3_batch_translate_text_with_model_test.py b/samples/snippets/translate_v3_batch_translate_text_with_model_test.py new file mode 100644 index 00000000..74b044f4 --- /dev/null +++ b/samples/snippets/translate_v3_batch_translate_text_with_model_test.py @@ -0,0 +1,46 @@ +# Copyright 2020 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 +# +# http://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 os +import pytest +import uuid +import translate_v3_batch_translate_text_with_model +from google.cloud import storage + +PROJECT_ID = os.environ["GCLOUD_PROJECT"] +MODEL_ID = "TRL3128559826197068699" + + +@pytest.fixture(scope="function") +def bucket(): + """Create a temporary bucket to store annotation output.""" + bucket_name = str(uuid.uuid1()) + storage_client = storage.Client() + bucket = storage_client.create_bucket(bucket_name) + + yield bucket + + bucket.delete(force=True) + + +def test_batch_translate_text_with_model(capsys, bucket): + translate_v3_batch_translate_text_with_model.batch_translate_text_with_model( + "gs://cloud-samples-data/translation/custom_model_text.txt", + "gs://{}/translation/BATCH_TRANSLATION_OUTPUT/".format(bucket.name), + PROJECT_ID, + MODEL_ID, + ) + out, _ = capsys.readouterr() + assert "Total Characters: 15" in out + assert "Translated Characters: 15" in out From fc25f6ae59e327ac731ba9a20ea82010fcda0baa Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 6 Mar 2020 19:04:23 +0100 Subject: [PATCH 16/50] Update dependency google-cloud-automl to v0.10.0 [(#3033)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/3033) Co-authored-by: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- samples/snippets/requirements.txt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 8e10deaa..8142930f 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,9 +1,3 @@ -<<<<<<< HEAD google-cloud-translate==3.0.0 google-cloud-storage==1.30.0 -google-cloud-automl==1.0.1 -======= -google-cloud-translate==2.0.0 -google-cloud-storage==1.19.1 -google-cloud-automl==0.9.0 ->>>>>>> 65dd67b5 (Translate: migrate published v3 translate batch samples [(#2914)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/2914)) +google-cloud-automl==1.0.1 \ No newline at end of file From 994e272293b8ad535267716f13ad9091b11f227d Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 9 Mar 2020 18:29:59 +0100 Subject: [PATCH 17/50] chore(deps): update dependency google-cloud-storage to v1.26.0 [(#3046)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/3046) * chore(deps): update dependency google-cloud-storage to v1.26.0 * chore(deps): specify dependencies by python version * chore: up other deps to try to remove errors Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> Co-authored-by: Leah Cole --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 8142930f..ca4e6935 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,3 @@ google-cloud-translate==3.0.0 google-cloud-storage==1.30.0 -google-cloud-automl==1.0.1 \ No newline at end of file +google-cloud-automl==1.0.1 From 7248bd4b7c5f50e751de8da785c2795209177a28 Mon Sep 17 00:00:00 2001 From: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> Date: Wed, 1 Apr 2020 19:11:50 -0700 Subject: [PATCH 18/50] Simplify noxfile setup. [(#2806)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/2806) * chore(deps): update dependency requests to v2.23.0 * Simplify noxfile and add version control. * Configure appengine/standard to only test Python 2.7. * Update Kokokro configs to match noxfile. * Add requirements-test to each folder. * Remove Py2 versions from everything execept appengine/standard. * Remove conftest.py. * Remove appengine/standard/conftest.py * Remove 'no-sucess-flaky-report' from pytest.ini. * Add GAE SDK back to appengine/standard tests. * Fix typo. * Roll pytest to python 2 version. * Add a bunch of testing requirements. * Remove typo. * Add appengine lib directory back in. * Add some additional requirements. * Fix issue with flake8 args. * Even more requirements. * Readd appengine conftest.py. * Add a few more requirements. * Even more Appengine requirements. * Add webtest for appengine/standard/mailgun. * Add some additional requirements. * Add workaround for issue with mailjet-rest. * Add responses for appengine/standard/mailjet. Co-authored-by: Renovate Bot --- samples/snippets/requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements-test.txt b/samples/snippets/requirements-test.txt index b267f58b..bd6c7999 100644 --- a/samples/snippets/requirements-test.txt +++ b/samples/snippets/requirements-test.txt @@ -1,3 +1,3 @@ backoff==1.10.0 flaky==3.7.0 -pytest==5.4.3 \ No newline at end of file +pytest==5.4.3 From db8474561d6af3879db83cf273b4a6fb0e2877c3 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 29 Apr 2020 07:26:36 +0200 Subject: [PATCH 19/50] chore(deps): update dependency google-cloud-storage to v1.28.0 [(#3260)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/3260) Co-authored-by: Takashi Matsuo --- samples/snippets/requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index ca4e6935..4d6589de 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,9 @@ +<<<<<<< HEAD google-cloud-translate==3.0.0 google-cloud-storage==1.30.0 google-cloud-automl==1.0.1 +======= +google-cloud-translate==2.0.1 +google-cloud-storage==1.28.0 +google-cloud-automl==0.10.0 +>>>>>>> 1a45813d (chore(deps): update dependency google-cloud-storage to v1.28.0 [(#3260)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/3260)) From 9ffb2da93ef28968cf1c363e4f4036b076008ad3 Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Tue, 12 May 2020 21:38:39 -0700 Subject: [PATCH 20/50] chore: some lint fixes [(#3751)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/3751) * chore: some lint fixes * longer timeout, more retries * disable detect_test.py::test_async_detect_document --- .../translate_v3_batch_translate_text_with_model_test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/samples/snippets/translate_v3_batch_translate_text_with_model_test.py b/samples/snippets/translate_v3_batch_translate_text_with_model_test.py index 74b044f4..36c52ae2 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_model_test.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_model_test.py @@ -13,10 +13,13 @@ # limitations under the License. import os -import pytest import uuid -import translate_v3_batch_translate_text_with_model + from google.cloud import storage +import pytest + +import translate_v3_batch_translate_text_with_model + PROJECT_ID = os.environ["GCLOUD_PROJECT"] MODEL_ID = "TRL3128559826197068699" From e2c689cf4dad485cdbd130212139bdbd3796cfb4 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 19 May 2020 04:18:01 +0200 Subject: [PATCH 21/50] chore(deps): update dependency google-cloud-storage to v1.28.1 [(#3785)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/3785) * chore(deps): update dependency google-cloud-storage to v1.28.1 * [asset] testing: use uuid instead of time Co-authored-by: Takashi Matsuo --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 4d6589de..1b208498 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -4,6 +4,6 @@ google-cloud-storage==1.30.0 google-cloud-automl==1.0.1 ======= google-cloud-translate==2.0.1 -google-cloud-storage==1.28.0 +google-cloud-storage==1.28.1 google-cloud-automl==0.10.0 >>>>>>> 1a45813d (chore(deps): update dependency google-cloud-storage to v1.28.0 [(#3260)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/3260)) From 9d2d2a3178a11ca0f2f5fda03158db7e192754e7 Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Mon, 8 Jun 2020 18:53:04 -0700 Subject: [PATCH 22/50] testing: start using btlr [(#3959)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/3959) * testing: start using btlr The binary is at gs://cloud-devrel-kokoro-resources/btlr/v0.0.1/btlr * add period after DIFF_FROM * use array for btlr args * fix websocket tests * add debug message * wait longer for the server to spin up * dlp: bump the wait timeout to 10 minutes * [run] copy noxfile.py to child directory to avoid gcloud issue * [iam] fix: only display description when the key exists * use uuid4 instead of uuid1 * [iot] testing: use the same format for registry id * Stop asserting Out of memory not in the output * fix missing imports * [dns] testing: more retries with delay * [dlp] testing: longer timeout * use the max-concurrency flag * use 30 workers * [monitoring] use multiple projects * [dlp] testing: longer timeout --- .../translate_v3_batch_translate_text_with_model_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/translate_v3_batch_translate_text_with_model_test.py b/samples/snippets/translate_v3_batch_translate_text_with_model_test.py index 36c52ae2..a7801f60 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_model_test.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_model_test.py @@ -28,7 +28,7 @@ @pytest.fixture(scope="function") def bucket(): """Create a temporary bucket to store annotation output.""" - bucket_name = str(uuid.uuid1()) + bucket_name = f'tmp-{uuid.uuid4().hex}' storage_client = storage.Client() bucket = storage_client.create_bucket(bucket_name) From 7d6b49e231ee05297d4b076c5fc61d5f9b08f699 Mon Sep 17 00:00:00 2001 From: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> Date: Tue, 9 Jun 2020 14:34:27 -0700 Subject: [PATCH 23/50] Replace GCLOUD_PROJECT with GOOGLE_CLOUD_PROJECT. [(#4022)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/4022) --- samples/snippets/dataset_test.py | 2 +- samples/snippets/model_test.py | 2 +- samples/snippets/predict_test.py | 2 +- .../translate_v3_batch_translate_text_with_model_test.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/snippets/dataset_test.py b/samples/snippets/dataset_test.py index 29e3e5c9..4430ec54 100644 --- a/samples/snippets/dataset_test.py +++ b/samples/snippets/dataset_test.py @@ -21,7 +21,7 @@ import automl_translation_dataset -project_id = os.environ["GCLOUD_PROJECT"] +project_id = os.environ["GOOGLE_CLOUD_PROJECT"] compute_region = "us-central1" diff --git a/samples/snippets/model_test.py b/samples/snippets/model_test.py index 0d37a85c..e19a50ea 100644 --- a/samples/snippets/model_test.py +++ b/samples/snippets/model_test.py @@ -22,7 +22,7 @@ import automl_translation_model -project_id = os.environ["GCLOUD_PROJECT"] +project_id = os.environ["GOOGLE_CLOUD_PROJECT"] compute_region = "us-central1" diff --git a/samples/snippets/predict_test.py b/samples/snippets/predict_test.py index f9d98dfb..d00a4658 100644 --- a/samples/snippets/predict_test.py +++ b/samples/snippets/predict_test.py @@ -18,7 +18,7 @@ import automl_translation_predict -project_id = os.environ["GCLOUD_PROJECT"] +project_id = os.environ["GOOGLE_CLOUD_PROJECT"] compute_region = "us-central1" diff --git a/samples/snippets/translate_v3_batch_translate_text_with_model_test.py b/samples/snippets/translate_v3_batch_translate_text_with_model_test.py index a7801f60..4d0def04 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_model_test.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_model_test.py @@ -21,7 +21,7 @@ import translate_v3_batch_translate_text_with_model -PROJECT_ID = os.environ["GCLOUD_PROJECT"] +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] MODEL_ID = "TRL3128559826197068699" From 033247100470b66cc25c443a8889b3201af35b62 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sat, 20 Jun 2020 01:03:47 +0200 Subject: [PATCH 24/50] chore(deps): update dependency google-cloud-storage to v1.29.0 [(#4040)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/4040) --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 1b208498..c8e8bbae 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -4,6 +4,6 @@ google-cloud-storage==1.30.0 google-cloud-automl==1.0.1 ======= google-cloud-translate==2.0.1 -google-cloud-storage==1.28.1 +google-cloud-storage==1.29.0 google-cloud-automl==0.10.0 >>>>>>> 1a45813d (chore(deps): update dependency google-cloud-storage to v1.28.0 [(#3260)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/3260)) From 0a43aeb656392dd755c47302bbac5e26f07b642f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sat, 20 Jun 2020 01:16:04 +0200 Subject: [PATCH 25/50] chore(deps): update dependency google-cloud-automl to v1 [(#4127)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/4127) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [google-cloud-automl](https://togithub.com/googleapis/python-automl) | major | `==0.10.0` -> `==1.0.1` | ---
googleapis/python-automl [Compare Source](https://togithub.com/googleapis/python-automl/compare/v0.10.0...v1.0.1)
--- :date: **Schedule**: At any time (no schedule defined). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Never, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#GoogleCloudPlatform/python-docs-samples). --- samples/snippets/requirements.txt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index c8e8bbae..16f45f97 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,9 +1,3 @@ -<<<<<<< HEAD -google-cloud-translate==3.0.0 +oogle-cloud-translate==3.0.0 google-cloud-storage==1.30.0 google-cloud-automl==1.0.1 -======= -google-cloud-translate==2.0.1 -google-cloud-storage==1.29.0 -google-cloud-automl==0.10.0 ->>>>>>> 1a45813d (chore(deps): update dependency google-cloud-storage to v1.28.0 [(#3260)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/3260)) From e6647a6af6d1589cb0653b1e1a563653efc67300 Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Thu, 23 Jul 2020 11:23:31 -0700 Subject: [PATCH 26/50] fix(translate): fix a broken test [(#4360)](https://github.com/GoogleCloudPlatform/python-docs-samples/issues/4360) * fix(translate): fix a broken test fixes #4353 * use uuid * fix builds --- samples/snippets/dataset_test.py | 4 ++-- samples/snippets/model_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/snippets/dataset_test.py b/samples/snippets/dataset_test.py index 4430ec54..eb5796d5 100644 --- a/samples/snippets/dataset_test.py +++ b/samples/snippets/dataset_test.py @@ -14,8 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import datetime import os +import uuid import pytest @@ -28,7 +28,7 @@ @pytest.mark.slow def test_dataset_create_import_delete(capsys): # create dataset - dataset_name = "test_" + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + dataset_name = f"test_{uuid.uuid4().hex[:27]}" automl_translation_dataset.create_dataset( project_id, compute_region, dataset_name, "en", "ja" ) diff --git a/samples/snippets/model_test.py b/samples/snippets/model_test.py index e19a50ea..fd2fabc3 100644 --- a/samples/snippets/model_test.py +++ b/samples/snippets/model_test.py @@ -77,4 +77,4 @@ def test_model_list_get_evaluate(capsys): project_id, compute_region, model_id, model_evaluation_id ) out, _ = capsys.readouterr() - assert "evaluation_metric" in out + assert model_evaluation_id in out From 6cf97421bf82bf07e5f2dc2db24cac1206c95466 Mon Sep 17 00:00:00 2001 From: Bu Sun Kim Date: Mon, 17 Aug 2020 22:24:49 +0000 Subject: [PATCH 27/50] chore: remove automl samples --- .../snippets/automl_translation_dataset.py | 278 ---------------- samples/snippets/automl_translation_model.py | 300 ------------------ .../snippets/automl_translation_predict.py | 84 ----- samples/snippets/dataset_test.py | 69 ---- samples/snippets/model_test.py | 80 ----- samples/snippets/predict_test.py | 31 -- samples/snippets/resources/input.txt | 1 - 7 files changed, 843 deletions(-) delete mode 100755 samples/snippets/automl_translation_dataset.py delete mode 100755 samples/snippets/automl_translation_model.py delete mode 100644 samples/snippets/automl_translation_predict.py delete mode 100644 samples/snippets/dataset_test.py delete mode 100644 samples/snippets/model_test.py delete mode 100644 samples/snippets/predict_test.py delete mode 100644 samples/snippets/resources/input.txt diff --git a/samples/snippets/automl_translation_dataset.py b/samples/snippets/automl_translation_dataset.py deleted file mode 100755 index cf3e50ae..00000000 --- a/samples/snippets/automl_translation_dataset.py +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2018 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 -# -# http://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. - -"""This application demonstrates how to perform basic operations on dataset -with the Google AutoML Translation API. - -For more information, see the documentation at -https://cloud.google.com/translate/automl/docs -""" - -import argparse -import os - - -def create_dataset(project_id, compute_region, dataset_name, source, target): - """Create a dataset.""" - # [START automl_translate_create_dataset] - # TODO(developer): Uncomment and set the following variables - # project_id = 'PROJECT_ID_HERE' - # compute_region = 'COMPUTE_REGION_HERE' - # dataset_name = 'DATASET_NAME_HERE' - # source = 'LANGUAGE_CODE_OF_SOURCE_LANGUAGE' - # target = 'LANGUAGE_CODE_OF_TARGET_LANGUAGE' - - from google.cloud import automl_v1beta1 as automl - - client = automl.AutoMlClient() - - # A resource that represents Google Cloud Platform location. - project_location = client.location_path(project_id, compute_region) - - # Specify the source and target language. - dataset_metadata = { - "source_language_code": source, - "target_language_code": target, - } - # Set dataset name and dataset metadata - my_dataset = { - "display_name": dataset_name, - "translation_dataset_metadata": dataset_metadata, - } - - # Create a dataset with the dataset metadata in the region. - dataset = client.create_dataset(project_location, my_dataset) - - # Display the dataset information - print("Dataset name: {}".format(dataset.name)) - print("Dataset id: {}".format(dataset.name.split("/")[-1])) - print("Dataset display name: {}".format(dataset.display_name)) - print("Translation dataset Metadata:") - print( - "\tsource_language_code: {}".format( - dataset.translation_dataset_metadata.source_language_code - ) - ) - print( - "\ttarget_language_code: {}".format( - dataset.translation_dataset_metadata.target_language_code - ) - ) - print("Dataset create time:") - print("\tseconds: {}".format(dataset.create_time.seconds)) - print("\tnanos: {}".format(dataset.create_time.nanos)) - - # [END automl_translate_create_dataset] - - -def list_datasets(project_id, compute_region, filter_): - """List Datasets.""" - # [START automl_translate_list_datasets] - # TODO(developer): Uncomment and set the following variables - # project_id = 'PROJECT_ID_HERE' - # compute_region = 'COMPUTE_REGION_HERE' - # filter_ = 'filter expression here' - - from google.cloud import automl_v1beta1 as automl - - client = automl.AutoMlClient() - - # A resource that represents Google Cloud Platform location. - project_location = client.location_path(project_id, compute_region) - - # List all the datasets available in the region by applying filter. - response = client.list_datasets(project_location, filter_) - - print("List of datasets:") - for dataset in response: - # Display the dataset information - print("Dataset name: {}".format(dataset.name)) - print("Dataset id: {}".format(dataset.name.split("/")[-1])) - print("Dataset display name: {}".format(dataset.display_name)) - print("Translation dataset metadata:") - print( - "\tsource_language_code: {}".format( - dataset.translation_dataset_metadata.source_language_code - ) - ) - print( - "\ttarget_language_code: {}".format( - dataset.translation_dataset_metadata.target_language_code - ) - ) - print("Dataset create time:") - print("\tseconds: {}".format(dataset.create_time.seconds)) - print("\tnanos: {}".format(dataset.create_time.nanos)) - - # [END automl_translate_list_datasets] - - -def get_dataset(project_id, compute_region, dataset_id): - """Get the dataset.""" - # [START automl_translate_get_dataset] - # TODO(developer): Uncomment and set the following variables - # project_id = 'PROJECT_ID_HERE' - # compute_region = 'COMPUTE_REGION_HERE' - # dataset_id = 'DATASET_ID_HERE' - - from google.cloud import automl_v1beta1 as automl - - client = automl.AutoMlClient() - - # Get the full path of the dataset - dataset_full_id = client.dataset_path( - project_id, compute_region, dataset_id - ) - - # Get complete detail of the dataset. - dataset = client.get_dataset(dataset_full_id) - - # Display the dataset information - print("Dataset name: {}".format(dataset.name)) - print("Dataset id: {}".format(dataset.name.split("/")[-1])) - print("Dataset display name: {}".format(dataset.display_name)) - print("Translation dataset metadata:") - print( - "\tsource_language_code: {}".format( - dataset.translation_dataset_metadata.source_language_code - ) - ) - print( - "\ttarget_language_code: {}".format( - dataset.translation_dataset_metadata.target_language_code - ) - ) - print("Dataset create time:") - print("\tseconds: {}".format(dataset.create_time.seconds)) - print("\tnanos: {}".format(dataset.create_time.nanos)) - - # [END automl_translate_get_dataset] - - -def import_data(project_id, compute_region, dataset_id, path): - """Import sentence pairs to the dataset.""" - # [START automl_translate_import_data] - # TODO(developer): Uncomment and set the following variables - # project_id = 'PROJECT_ID_HERE' - # compute_region = 'COMPUTE_REGION_HERE' - # dataset_id = 'DATASET_ID_HERE' - # path = 'gs://path/to/file.csv' - - from google.cloud import automl_v1beta1 as automl - - client = automl.AutoMlClient() - - # Get the full path of the dataset. - dataset_full_id = client.dataset_path( - project_id, compute_region, dataset_id - ) - - # Get the multiple Google Cloud Storage URIs - input_uris = path.split(",") - input_config = {"gcs_source": {"input_uris": input_uris}} - - # Import data from the input URI - response = client.import_data(dataset_full_id, input_config) - - print("Processing import...") - # synchronous check of operation status - print("Data imported. {}".format(response.result())) - - # [END automl_translate_import_data] - - -def delete_dataset(project_id, compute_region, dataset_id): - """Delete a dataset.""" - # [START automl_translate_delete_dataset] - # TODO(developer): Uncomment and set the following variables - # project_id = 'PROJECT_ID_HERE' - # compute_region = 'COMPUTE_REGION_HERE' - # dataset_id = 'DATASET_ID_HERE' - - from google.cloud import automl_v1beta1 as automl - - client = automl.AutoMlClient() - - # Get the full path of the dataset. - dataset_full_id = client.dataset_path( - project_id, compute_region, dataset_id - ) - - # Delete a dataset. - response = client.delete_dataset(dataset_full_id) - - # synchronous check of operation status - print("Dataset deleted. {}".format(response.result())) - - # [END automl_translate_delete_dataset] - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - subparsers = parser.add_subparsers(dest="command") - - create_dataset_parser = subparsers.add_parser( - "create_dataset", help=create_dataset.__doc__ - ) - create_dataset_parser.add_argument("dataset_name") - create_dataset_parser.add_argument("source") - create_dataset_parser.add_argument("target") - - list_datasets_parser = subparsers.add_parser( - "list_datasets", help=list_datasets.__doc__ - ) - list_datasets_parser.add_argument("filter", nargs="?", default="") - - import_data_parser = subparsers.add_parser( - "import_data", help=import_data.__doc__ - ) - import_data_parser.add_argument("dataset_id") - import_data_parser.add_argument("path") - - delete_dataset_parser = subparsers.add_parser( - "delete_dataset", help=delete_dataset.__doc__ - ) - delete_dataset_parser.add_argument("dataset_id") - - get_dataset_parser = subparsers.add_parser( - "get_dataset", help=get_dataset.__doc__ - ) - get_dataset_parser.add_argument("dataset_id") - - project_id = os.environ["PROJECT_ID"] - compute_region = os.environ["REGION_NAME"] - - args = parser.parse_args() - - if args.command == "create_dataset": - create_dataset( - project_id, - compute_region, - args.dataset_name, - args.source, - args.target, - ) - if args.command == "list_datasets": - list_datasets(project_id, compute_region, args.filter) - if args.command == "get_dataset": - get_dataset(project_id, compute_region, args.dataset_id) - if args.command == "import_data": - import_data(project_id, compute_region, args.dataset_id, args.path) - if args.command == "delete_dataset": - delete_dataset(project_id, compute_region, args.dataset_id) diff --git a/samples/snippets/automl_translation_model.py b/samples/snippets/automl_translation_model.py deleted file mode 100755 index 77a4ed73..00000000 --- a/samples/snippets/automl_translation_model.py +++ /dev/null @@ -1,300 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2018 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 -# -# http://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. - -"""This application demonstrates how to perform basic operations on model -with the Google AutoML Translation API. - -For more information, see the documentation at -https://cloud.google.com/translate/automl/docs -""" - -import argparse -import os - - -def create_model(project_id, compute_region, dataset_id, model_name): - """Create a model.""" - # [START automl_translate_create_model] - # TODO(developer): Uncomment and set the following variables - # project_id = 'PROJECT_ID_HERE' - # compute_region = 'COMPUTE_REGION_HERE' - # dataset_id = 'DATASET_ID_HERE' - # model_name = 'MODEL_NAME_HERE' - - from google.cloud import automl_v1beta1 as automl - - client = automl.AutoMlClient() - - # A resource that represents Google Cloud Platform location. - project_location = client.location_path(project_id, compute_region) - - # Set model name and dataset. - my_model = { - "display_name": model_name, - "dataset_id": dataset_id, - "translation_model_metadata": {"base_model": ""}, - } - - # Create a model with the model metadata in the region. - response = client.create_model(project_location, my_model) - - print("Training operation name: {}".format(response.operation.name)) - print("Training started...") - - # [END automl_translate_create_model] - - -def list_models(project_id, compute_region, filter_): - """List all models.""" - # [START automl_translate_list_models] - # TODO(developer): Uncomment and set the following variables - # project_id = 'PROJECT_ID_HERE' - # compute_region = 'COMPUTE_REGION_HERE' - # filter_ = 'DATASET_ID_HERE' - - from google.cloud import automl_v1beta1 as automl - from google.cloud.automl_v1beta1 import enums - - client = automl.AutoMlClient() - - # A resource that represents Google Cloud Platform location. - project_location = client.location_path(project_id, compute_region) - - # List all the models available in the region by applying filter. - response = client.list_models(project_location, filter_) - - print("List of models:") - for model in response: - # Display the model information. - if model.deployment_state == enums.Model.DeploymentState.DEPLOYED: - deployment_state = "deployed" - else: - deployment_state = "undeployed" - - print("Model name: {}".format(model.name)) - print("Model id: {}".format(model.name.split("/")[-1])) - print("Model display name: {}".format(model.display_name)) - print("Model create time:") - print("\tseconds: {}".format(model.create_time.seconds)) - print("\tnanos: {}".format(model.create_time.nanos)) - print("Model deployment state: {}".format(deployment_state)) - - # [END automl_translate_list_models] - - -def get_model(project_id, compute_region, model_id): - """Get model details.""" - # [START automl_translate_get_model] - # TODO(developer): Uncomment and set the following variables - # project_id = 'PROJECT_ID_HERE' - # compute_region = 'COMPUTE_REGION_HERE' - # model_id = 'MODEL_ID_HERE' - - from google.cloud import automl_v1beta1 as automl - from google.cloud.automl_v1beta1 import enums - - client = automl.AutoMlClient() - - # Get the full path of the model. - model_full_id = client.model_path(project_id, compute_region, model_id) - - # Get complete detail of the model. - model = client.get_model(model_full_id) - - # Retrieve deployment state. - if model.deployment_state == enums.Model.DeploymentState.DEPLOYED: - deployment_state = "deployed" - else: - deployment_state = "undeployed" - - # Display the model information. - print("Model name: {}".format(model.name)) - print("Model id: {}".format(model.name.split("/")[-1])) - print("Model display name: {}".format(model.display_name)) - print("Model create time:") - print("\tseconds: {}".format(model.create_time.seconds)) - print("\tnanos: {}".format(model.create_time.nanos)) - print("Model deployment state: {}".format(deployment_state)) - - # [END automl_translate_get_model] - - -def list_model_evaluations(project_id, compute_region, model_id, filter_): - """List model evaluations.""" - # [START automl_translate_list_model_evaluations] - # TODO(developer): Uncomment and set the following variables - # project_id = 'PROJECT_ID_HERE' - # compute_region = 'COMPUTE_REGION_HERE' - # model_id = 'MODEL_ID_HERE' - # filter_ = 'filter expression here' - - from google.cloud import automl_v1beta1 as automl - - client = automl.AutoMlClient() - - # Get the full path of the model. - model_full_id = client.model_path(project_id, compute_region, model_id) - - print("List of model evaluations:") - for element in client.list_model_evaluations(model_full_id, filter_): - print(element) - - # [END automl_translate_list_model_evaluations] - - -def get_model_evaluation( - project_id, compute_region, model_id, model_evaluation_id -): - """Get model evaluation.""" - # [START automl_translate_get_model_evaluation] - # TODO(developer): Uncomment and set the following variables - # project_id = 'PROJECT_ID_HERE' - # compute_region = 'COMPUTE_REGION_HERE' - # model_id = 'MODEL_ID_HERE' - # model_evaluation_id = 'MODEL_EVALUATION_ID_HERE' - - from google.cloud import automl_v1beta1 as automl - - client = automl.AutoMlClient() - - # Get the full path of the model evaluation. - model_evaluation_full_id = client.model_evaluation_path( - project_id, compute_region, model_id, model_evaluation_id - ) - - # Get complete detail of the model evaluation. - response = client.get_model_evaluation(model_evaluation_full_id) - - print(response) - - # [END automl_translate_get_model_evaluation] - - -def delete_model(project_id, compute_region, model_id): - """Delete a model.""" - # [START automl_translate_delete_model] - # TODO(developer): Uncomment and set the following variables - # project_id = 'PROJECT_ID_HERE' - # compute_region = 'COMPUTE_REGION_HERE' - # model_id = 'MODEL_ID_HERE' - - from google.cloud import automl_v1beta1 as automl - - client = automl.AutoMlClient() - - # Get the full path of the model. - model_full_id = client.model_path(project_id, compute_region, model_id) - - # Delete a model. - response = client.delete_model(model_full_id) - - # synchronous check of operation status. - print("Model deleted. {}".format(response.result())) - - # [END automl_translate_delete_model] - - -def get_operation_status(operation_full_id): - """Get operation status.""" - # [START automl_translate_get_operation_status] - # TODO(developer): Uncomment and set the following variables - # operation_full_id = - # 'projects//locations//operations/' - - from google.cloud import automl_v1beta1 as automl - - client = automl.AutoMlClient() - - # Get the latest state of a long-running operation. - response = client.transport._operations_client.get_operation( - operation_full_id - ) - - print("Operation status: {}".format(response)) - - # [END automl_translate_get_operation_status] - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - subparsers = parser.add_subparsers(dest="command") - - create_model_parser = subparsers.add_parser( - "create_model", help=create_model.__doc__ - ) - create_model_parser.add_argument("dataset_id") - create_model_parser.add_argument("model_name") - - list_model_evaluations_parser = subparsers.add_parser( - "list_model_evaluations", help=list_model_evaluations.__doc__ - ) - list_model_evaluations_parser.add_argument("model_id") - list_model_evaluations_parser.add_argument("filter", nargs="?", default="") - - get_model_evaluation_parser = subparsers.add_parser( - "get_model_evaluation", help=get_model_evaluation.__doc__ - ) - get_model_evaluation_parser.add_argument("model_id") - get_model_evaluation_parser.add_argument("model_evaluation_id") - - get_model_parser = subparsers.add_parser( - "get_model", help=get_model.__doc__ - ) - get_model_parser.add_argument("model_id") - - get_operation_status_parser = subparsers.add_parser( - "get_operation_status", help=get_operation_status.__doc__ - ) - get_operation_status_parser.add_argument("operation_full_id") - - list_models_parser = subparsers.add_parser( - "list_models", help=list_models.__doc__ - ) - list_models_parser.add_argument("filter", nargs="?", default="") - - delete_model_parser = subparsers.add_parser( - "delete_model", help=delete_model.__doc__ - ) - delete_model_parser.add_argument("model_id") - - project_id = os.environ["PROJECT_ID"] - compute_region = os.environ["REGION_NAME"] - - args = parser.parse_args() - - if args.command == "create_model": - create_model( - project_id, compute_region, args.dataset_id, args.model_name - ) - if args.command == "list_models": - list_models(project_id, compute_region, args.filter) - if args.command == "get_model": - get_model(project_id, compute_region, args.model_id) - if args.command == "list_model_evaluations": - list_model_evaluations( - project_id, compute_region, args.model_id, args.filter - ) - if args.command == "get_model_evaluation": - get_model_evaluation( - project_id, compute_region, args.model_id, args.model_evaluation_id - ) - if args.command == "delete_model": - delete_model(project_id, compute_region, args.model_id) - if args.command == "get_operation_status": - get_operation_status(args.operation_full_id) diff --git a/samples/snippets/automl_translation_predict.py b/samples/snippets/automl_translation_predict.py deleted file mode 100644 index b15e0e30..00000000 --- a/samples/snippets/automl_translation_predict.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2018 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 -# -# http://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. - -"""This application demonstrates how to perform basic operations on prediction -with the Google AutoML Translation API. - -For more information, see the documentation at -https://cloud.google.com/translate/automl/docs -""" - -import argparse -import os - - -def predict(project_id, compute_region, model_id, file_path): - """Translate the content.""" - # [START automl_translate_predict] - # project_id = 'PROJECT_ID_HERE' - # compute_region = 'COMPUTE_REGION_HERE' - # model_id = 'MODEL_ID_HERE' - # file_path = '/local/path/to/file' - - from google.cloud import automl_v1beta1 as automl - - automl_client = automl.AutoMlClient() - - # Create client for prediction service. - prediction_client = automl.PredictionServiceClient() - - # Get the full path of the model. - model_full_id = automl_client.model_path( - project_id, compute_region, model_id - ) - - # Read the file content for translation. - with open(file_path, "rb") as content_file: - content = content_file.read() - content.decode("utf-8") - - # Set the payload by giving the content of the file. - payload = {"text_snippet": {"content": content}} - - # params is additional domain-specific parameters. - params = {} - - response = prediction_client.predict(model_full_id, payload, params) - translated_content = response.payload[0].translation.translated_content - - print(u"Translated content: {}".format(translated_content.content)) - - # [END automl_translate_predict] - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - subparsers = parser.add_subparsers(dest="command") - - predict_parser = subparsers.add_parser("predict", help=predict.__doc__) - predict_parser.add_argument("model_id") - predict_parser.add_argument("file_path") - - project_id = os.environ["PROJECT_ID"] - compute_region = os.environ["REGION_NAME"] - - args = parser.parse_args() - - if args.command == "predict": - predict(project_id, compute_region, args.model_id, args.file_path) diff --git a/samples/snippets/dataset_test.py b/samples/snippets/dataset_test.py deleted file mode 100644 index eb5796d5..00000000 --- a/samples/snippets/dataset_test.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2018 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 -# -# http://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 os -import uuid - -import pytest - -import automl_translation_dataset - -project_id = os.environ["GOOGLE_CLOUD_PROJECT"] -compute_region = "us-central1" - - -@pytest.mark.slow -def test_dataset_create_import_delete(capsys): - # create dataset - dataset_name = f"test_{uuid.uuid4().hex[:27]}" - automl_translation_dataset.create_dataset( - project_id, compute_region, dataset_name, "en", "ja" - ) - out, _ = capsys.readouterr() - create_dataset_output = out.splitlines() - assert "Dataset id: " in create_dataset_output[1] - - # import data - dataset_id = create_dataset_output[1].split()[2] - data = "gs://{}-vcm/en-ja.csv".format(project_id) - automl_translation_dataset.import_data( - project_id, compute_region, dataset_id, data - ) - out, _ = capsys.readouterr() - assert "Data imported." in out - - # delete dataset - automl_translation_dataset.delete_dataset( - project_id, compute_region, dataset_id - ) - out, _ = capsys.readouterr() - assert "Dataset deleted." in out - - -def test_dataset_list_get(capsys): - # list datasets - automl_translation_dataset.list_datasets(project_id, compute_region, "") - out, _ = capsys.readouterr() - list_dataset_output = out.splitlines() - assert "Dataset id: " in list_dataset_output[2] - - # get dataset - dataset_id = list_dataset_output[2].split()[2] - automl_translation_dataset.get_dataset( - project_id, compute_region, dataset_id - ) - out, _ = capsys.readouterr() - assert "Dataset name: " in out diff --git a/samples/snippets/model_test.py b/samples/snippets/model_test.py deleted file mode 100644 index fd2fabc3..00000000 --- a/samples/snippets/model_test.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2018 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 -# -# http://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 os - -from google.cloud import automl_v1beta1 as automl -import pytest - -import automl_translation_model - -project_id = os.environ["GOOGLE_CLOUD_PROJECT"] -compute_region = "us-central1" - - -@pytest.mark.skip(reason="creates too many models") -def test_model_create_status_delete(capsys): - # create model - client = automl.AutoMlClient() - model_name = "test_" + datetime.datetime.now().strftime("%Y%m%d%H%M%S") - project_location = client.location_path(project_id, compute_region) - my_model = { - "display_name": model_name, - "dataset_id": "3876092572857648864", - "translation_model_metadata": {"base_model": ""}, - } - response = client.create_model(project_location, my_model) - operation_name = response.operation.name - assert operation_name - - # get operation status - automl_translation_model.get_operation_status(operation_name) - out, _ = capsys.readouterr() - assert "Operation status: " in out - - # cancel operation - response.cancel() - - -def test_model_list_get_evaluate(capsys): - # list models - automl_translation_model.list_models(project_id, compute_region, "") - out, _ = capsys.readouterr() - list_models_output = out.splitlines() - assert "Model id: " in list_models_output[2] - - # get model - model_id = list_models_output[2].split()[2] - automl_translation_model.get_model(project_id, compute_region, model_id) - out, _ = capsys.readouterr() - assert "Model name: " in out - - # list model evaluations - automl_translation_model.list_model_evaluations( - project_id, compute_region, model_id, "" - ) - out, _ = capsys.readouterr() - list_evals_output = out.splitlines() - assert "name: " in list_evals_output[1] - - # get model evaluation - model_evaluation_id = list_evals_output[1].split("/")[-1][:-1] - automl_translation_model.get_model_evaluation( - project_id, compute_region, model_id, model_evaluation_id - ) - out, _ = capsys.readouterr() - assert model_evaluation_id in out diff --git a/samples/snippets/predict_test.py b/samples/snippets/predict_test.py deleted file mode 100644 index d00a4658..00000000 --- a/samples/snippets/predict_test.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2018 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 -# -# http://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 os - -import automl_translation_predict - -project_id = os.environ["GOOGLE_CLOUD_PROJECT"] -compute_region = "us-central1" - - -def test_predict(capsys): - model_id = "TRL3128559826197068699" - automl_translation_predict.predict( - project_id, compute_region, model_id, "resources/input.txt" - ) - out, _ = capsys.readouterr() - assert "Translated content: " in out diff --git a/samples/snippets/resources/input.txt b/samples/snippets/resources/input.txt deleted file mode 100644 index 5aecd659..00000000 --- a/samples/snippets/resources/input.txt +++ /dev/null @@ -1 +0,0 @@ -Tell me how this ends \ No newline at end of file From 1e030d4557ee1f67bad5e5b4759d0200efd27afd Mon Sep 17 00:00:00 2001 From: Bu Sun Kim Date: Tue, 18 Aug 2020 00:21:44 +0000 Subject: [PATCH 28/50] docs: add w/ glossary and model --- ..._translate_text_with_glossary_and_model.py | 137 ++++++++++++++++++ ...slate_text_with_glossary_and_model_test.py | 72 +++++++++ 2 files changed, 209 insertions(+) create mode 100644 samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py create mode 100644 samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py diff --git a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py new file mode 100644 index 00000000..72ddcfee --- /dev/null +++ b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py @@ -0,0 +1,137 @@ +# -*- 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. + +# DO NOT EDIT! This is a generated sample ("LongRunningPromise", "translate_v3_batch_translate_text_with_glossary_and_model") + +# To install the latest published package dependency, execute the following: +# pip install google-cloud-translate + +# sample-metadata +# title: Batch Translate with Glossary and Model +# description: Batch translate text with Glossary using AutoML Translation model +# usage: python3 translate_v3_batch_translate_text_with_glossary_and_model.py [--input_uri "gs://cloud-samples-data/text.txt"] [--output_uri "gs://YOUR_BUCKET_ID/path_to_store_results/"] [--project "[Google Cloud Project ID]"] [--location "us-central1"] [--target_language en] [--source_language de] [--model_id "{your-model-id}"] [--glossary_id "{your-glossary-id}"] + +# [START translate_v3_batch_translate_text_with_glossary_and_model] +from google.cloud import translate + +def sample_batch_translate_text_with_glossary_and_model( + input_uri, + output_uri, + project_id, + location, + target_language, + source_language, + model_id, + glossary_id +): + """ + Batch translate text with Glossary and Translation model + """ + + client = translate.TranslationServiceClient() + + # TODO(developer): Uncomment and set the following variables + # input_uri = 'gs://cloud-samples-data/text.txt' + # output_uri = 'gs://YOUR_BUCKET_ID/path_to_store_results/' + # project = '[Google Cloud Project ID]' + # location = 'us-central1' + # target_language = 'en' + # source_language = 'de' + # model_id = '{your-model-id}' + # glossary_id = '[YOUR_GLOSSARY_ID]' + target_language_codes = [target_language] + gcs_source = {"input_uri": input_uri} + + # Optional. Can be "text/plain" or "text/html". + mime_type = "text/plain" + input_configs_element = {"gcs_source": gcs_source, "mime_type": mime_type} + input_configs = [input_configs_element] + gcs_destination = {"output_uri_prefix": output_uri} + output_config = {"gcs_destination": gcs_destination} + parent = client.location_path(project_id, location) + model_path = 'projects/{}/locations/{}/models/{}'.format(project_id, 'us-central1', model_id) + models = {target_language: model_path} + + glossary_path = client.glossary_path( + project_id, + 'us-central1', # The location of the glossary + glossary_id) + + glossary_config = translate.types.TranslateTextGlossaryConfig( + glossary=glossary_path) + glossaries = {"ja": glossary_config} #target lang as key + + operation = client.batch_translate_text( + parent=parent, + source_language_code=source_language, + target_language_codes=target_language_codes, + input_configs=input_configs, + output_config=output_config, + models=models, + glossaries=glossaries + ) + + print(u"Waiting for operation to complete...") + response = operation.result() + + # Display the translation for each input text provided + print(u"Total Characters: {}".format(response.total_characters)) + print(u"Translated Characters: {}".format(response.translated_characters)) + + +# [END translate_v3_batch_translate_text_with_glossary_and_model] + + +def main(): + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument( + "--input_uri", type=str, default="gs://cloud-samples-data/text.txt" + ) + parser.add_argument( + "--output_uri", type=str, default="gs://YOUR_BUCKET_ID/path_to_store_results/" + ) + parser.add_argument("--project_id", type=str, default="[Google Cloud Project ID]") + parser.add_argument("--location", type=str, default="us-central1") + parser.add_argument("--target_language", type=str, default="en") + parser.add_argument("--source_language", type=str, default="de") + parser.add_argument( + "--model_id", + type=str, + default="{your-model-id}" + ) + parser.add_argument( + "--glossary_id", + type=str, + default="[YOUR_GLOSSARY_ID]", + ) + args = parser.parse_args() + + sample_batch_translate_text_with_glossary_and_model( + args.input_uri, + args.output_uri, + args.project_id, + args.location, + args.target_language, + args.source_language, + args.model_id, + args.glossary_id + ) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py new file mode 100644 index 00000000..563c9f74 --- /dev/null +++ b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py @@ -0,0 +1,72 @@ +# 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 +# +# http://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 os +import pytest +import uuid +import translate_v3_batch_translate_text_with_glossary_and_model +import translate_v3_create_glossary +import translate_v3_delete_glossary +from google.cloud import storage + +PROJECT_ID = os.environ['GOOGLE_CLOUD_PROJECT'] +GLOSSARY_INPUT_URI = 'gs://cloud-samples-data/translation/glossary_ja.csv' +MODEL_ID = 'TRL3128559826197068699' + +@pytest.fixture(scope='session') +def glossary(): + """Get the ID of a glossary available to session (do not mutate/delete).""" + glossary_id = 'must-start-with-letters-' + str(uuid.uuid1()) + translate_v3_create_glossary.sample_create_glossary( + PROJECT_ID, + GLOSSARY_INPUT_URI, + glossary_id + ) + + yield glossary_id + + try: + translate_v3_delete_glossary.sample_delete_glossary(PROJECT_ID, glossary_id) + except Exception: + pass + +@pytest.fixture(scope='function') +def bucket(): + """Create a temporary bucket to store annotation output.""" + bucket_name = "mike-test-delete-" + str(uuid.uuid1()) + storage_client = storage.Client() + bucket = storage_client.create_bucket(bucket_name) + + yield bucket + + bucket.delete(force=True) + +def test_batch_translate_text_with_glossary_and_model(capsys, bucket, glossary): + translate_v3_batch_translate_text_with_glossary_and_model.sample_batch_translate_text_with_glossary_and_model( + 'gs://cloud-samples-data/translation/text_with_custom_model_and_glossary.txt', + 'gs://{}/translation/BATCH_TRANSLATION_OUTPUT/'.format(bucket.name), + PROJECT_ID, + 'us-central1', + 'ja', + 'en', + MODEL_ID, + glossary + ) + + out, _ = capsys.readouterr() + assert 'Total Characters: 25' in out + #TODO: find a way to make sure it translates correctly + # SHOULD NOT BE - Google NMT model -> γγ‚Œγ―γ—γΎγ›γ‚“γ€‚ζ¬Ίception" + # literal: "γγ‚Œγ―γγ†γ " # custom model + # literal: "欺く" # glossary \ No newline at end of file From e679ff4945c132770488f10364c16112e27a6137 Mon Sep 17 00:00:00 2001 From: Bu Sun Kim Date: Tue, 18 Aug 2020 00:40:42 +0000 Subject: [PATCH 29/50] test: fix sample test --- ...translate_v3_batch_translate_text_with_glossary_and_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py index 72ddcfee..acb53fcd 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py @@ -61,7 +61,7 @@ def sample_batch_translate_text_with_glossary_and_model( input_configs = [input_configs_element] gcs_destination = {"output_uri_prefix": output_uri} output_config = {"gcs_destination": gcs_destination} - parent = client.location_path(project_id, location) + parent = f"projects/{project_id}/locations/location" model_path = 'projects/{}/locations/{}/models/{}'.format(project_id, 'us-central1', model_id) models = {target_language: model_path} From 7bb741a82f0f7a44559d8a3102a7557bfc22ba3b Mon Sep 17 00:00:00 2001 From: Bu Sun Kim Date: Tue, 18 Aug 2020 01:19:29 +0000 Subject: [PATCH 30/50] test: fix remaining --- samples/snippets/beta_snippets.py | 246 +++++++++--------- samples/snippets/beta_snippets_test.py | 54 ++-- .../hybrid_glossaries/hybrid_tutorial.py | 89 +++---- .../hybrid_glossaries/hybrid_tutorial_test.py | 37 ++- samples/snippets/hybrid_glossaries/noxfile.py | 26 +- samples/snippets/noxfile.py | 26 +- samples/snippets/quickstart.py | 14 +- samples/snippets/quickstart_test.py | 2 +- samples/snippets/snippets.py | 77 +++--- samples/snippets/snippets_test.py | 20 +- .../translate_v3_batch_translate_text.py | 18 +- .../translate_v3_batch_translate_text_test.py | 2 +- ...e_v3_batch_translate_text_with_glossary.py | 24 +- ..._translate_text_with_glossary_and_model.py | 57 ++-- ...slate_text_with_glossary_and_model_test.py | 51 ++-- ...batch_translate_text_with_glossary_test.py | 8 +- ...late_v3_batch_translate_text_with_model.py | 24 +- ...v3_batch_translate_text_with_model_test.py | 2 +- .../snippets/translate_v3_create_glossary.py | 8 +- .../translate_v3_create_glossary_test.py | 4 +- .../snippets/translate_v3_delete_glossary.py | 4 +- .../snippets/translate_v3_detect_language.py | 4 +- .../translate_v3_get_glossary_test.py | 4 +- .../translate_v3_get_supported_languages.py | 2 +- ..._v3_get_supported_languages_with_target.py | 8 +- ...et_supported_languages_with_target_test.py | 4 +- .../translate_v3_list_glossary_test.py | 4 +- .../snippets/translate_v3_translate_text.py | 4 +- .../translate_v3_translate_text_test.py | 3 +- ...anslate_v3_translate_text_with_glossary.py | 7 +- ...te_v3_translate_text_with_glossary_test.py | 4 +- .../translate_v3_translate_text_with_model.py | 3 +- 32 files changed, 419 insertions(+), 421 deletions(-) diff --git a/samples/snippets/beta_snippets.py b/samples/snippets/beta_snippets.py index 10ec2dc6..835e55b1 100644 --- a/samples/snippets/beta_snippets.py +++ b/samples/snippets/beta_snippets.py @@ -19,11 +19,12 @@ def translate_text(project_id, text): # [START translate_translate_text_beta] from google.cloud import translate_v3beta1 as translate + client = translate.TranslationServiceClient() # project_id = YOUR_PROJECT_ID # text = 'Text you wish to translate' - location = 'global' + location = "global" parent = f"projects/{project_id}/locations/{location}" @@ -33,38 +34,38 @@ def translate_text(project_id, text): "contents": [text], "mime_type": "text/plain", # mime types: text/plain, text/html "source_language_code": "en-US", - "target_language_code": "sr-Latn" + "target_language_code": "sr-Latn", } ) for translation in response.translations: - print(u'Translated Text: {}'.format(translation)) + print("Translated Text: {}".format(translation)) # [END translate_translate_text_beta] def batch_translate_text(project_id, input_uri, output_uri): # [START translate_batch_translate_text_beta] from google.cloud import translate_v3beta1 as translate + client = translate.TranslationServiceClient() # project_id = YOUR_PROJECT_ID # input_uri = 'gs://cloud-samples-data/translation/text.txt' # output_uri = 'gs://YOUR_BUCKET_ID/path_to_store_results/' - location = 'us-central1' + location = "us-central1" parent = f"projects/{project_id}/locations/{location}" gcs_source = translate.types.GcsSource(input_uri=input_uri) input_config = translate.types.InputConfig( - mime_type='text/plain', # mime types: text/plain, text/html - gcs_source=gcs_source) + mime_type="text/plain", # mime types: text/plain, text/html + gcs_source=gcs_source, + ) - gcs_destination = translate.types.GcsDestination( - output_uri_prefix=output_uri) + gcs_destination = translate.types.GcsDestination(output_uri_prefix=output_uri) - output_config = translate.types.OutputConfig( - gcs_destination=gcs_destination) + output_config = translate.types.OutputConfig(gcs_destination=gcs_destination) operation = client.batch_translate_text( request={ @@ -72,25 +73,26 @@ def batch_translate_text(project_id, input_uri, output_uri): "source_language_code": "en-US", "target_language_codes": ["sr-Latn"], "input_configs": [input_config], - "output_config": output_config + "output_config": output_config, } ) result = operation.result(timeout=240) - print(u'Total Characters: {}'.format(result.total_characters)) - print(u'Translated Characters: {}'.format(result.translated_characters)) + print("Total Characters: {}".format(result.total_characters)) + print("Translated Characters: {}".format(result.translated_characters)) # [END translate_batch_translate_text_beta] def detect_language(project_id, text): # [START translate_detect_language_beta] from google.cloud import translate_v3beta1 as translate + client = translate.TranslationServiceClient() # project_id = YOUR_PROJECT_ID # text = 'Text you wish to translate' - location = 'global' + location = "global" parent = f"projects/{project_id}/locations/{location}" @@ -98,176 +100,178 @@ def detect_language(project_id, text): request={ "parent": parent, "content": text, - "mime_type": "text/plain" # mime types: text/plain, text/html + "mime_type": "text/plain", # mime types: text/plain, text/html } ) for language in response.languages: - print(u'Language Code: {} (Confidence: {})'.format( - language.language_code, - language.confidence)) + print( + "Language Code: {} (Confidence: {})".format( + language.language_code, language.confidence + ) + ) # [END translate_detect_language_beta] def list_languages(project_id): # [START translate_list_codes_beta] from google.cloud import translate_v3beta1 as translate + client = translate.TranslationServiceClient() # project_id = YOUR_PROJECT_ID - location = 'global' + location = "global" parent = f"projects/{project_id}/locations/{location}" response = client.get_supported_languages(parent=parent) - print('Supported Languages:') + print("Supported Languages:") for language in response.languages: - print(u'Language Code: {}'.format(language.language_code)) + print("Language Code: {}".format(language.language_code)) # [END translate_list_codes_beta] def list_languages_with_target(project_id, display_language_code): # [START translate_list_language_names_beta] from google.cloud import translate_v3beta1 as translate + client = translate.TranslationServiceClient() # project_id = YOUR_PROJECT_ID # display_language_code = 'is' - location = 'global' + location = "global" parent = f"projects/{project_id}/locations/{location}" response = client.get_supported_languages( - parent=parent, - display_language_code=display_language_code) + parent=parent, display_language_code=display_language_code + ) - print('Supported Languages:') + print("Supported Languages:") for language in response.languages: - print(u'Language Code: {}'.format(language.language_code)) - print(u'Display Name: {}\n'.format(language.display_name)) + print("Language Code: {}".format(language.language_code)) + print("Display Name: {}\n".format(language.display_name)) # [END translate_list_language_names_beta] def create_glossary(project_id, glossary_id): # [START translate_create_glossary_beta] from google.cloud import translate_v3beta1 as translate + client = translate.TranslationServiceClient() # project_id = 'YOUR_PROJECT_ID' # glossary_id = 'glossary-id' - location = 'us-central1' # The location of the glossary + location = "us-central1" # The location of the glossary - name = client.glossary_path( - project_id, - location, - glossary_id) + name = client.glossary_path(project_id, location, glossary_id) language_codes_set = translate.types.Glossary.LanguageCodesSet( - language_codes=['en', 'es']) + language_codes=["en", "es"] + ) gcs_source = translate.types.GcsSource( - input_uri='gs://cloud-samples-data/translation/glossary.csv') + input_uri="gs://cloud-samples-data/translation/glossary.csv" + ) - input_config = translate.types.GlossaryInputConfig( - gcs_source=gcs_source) + input_config = translate.types.GlossaryInputConfig(gcs_source=gcs_source) glossary = translate.types.Glossary( - name=name, - language_codes_set=language_codes_set, - input_config=input_config) + name=name, language_codes_set=language_codes_set, input_config=input_config + ) parent = f"projects/{project_id}/locations/{location}" operation = client.create_glossary(parent=parent, glossary=glossary) result = operation.result(timeout=90) - print(u'Created: {}'.format(result.name)) - print(u'Input Uri: {}'.format(result.input_config.gcs_source.input_uri)) + print("Created: {}".format(result.name)) + print("Input Uri: {}".format(result.input_config.gcs_source.input_uri)) # [END translate_create_glossary_beta] def list_glossaries(project_id): # [START translate_list_glossary_beta] from google.cloud import translate_v3beta1 as translate + client = translate.TranslationServiceClient() # project_id = 'YOUR_PROJECT_ID' - location = 'us-central1' # The location of the glossary + location = "us-central1" # The location of the glossary parent = f"projects/{project_id}/locations/{location}" for glossary in client.list_glossaries(parent=parent): - print(u'Name: {}'.format(glossary.name)) - print(u'Entry count: {}'.format(glossary.entry_count)) - print(u'Input uri: {}'.format( - glossary.input_config.gcs_source.input_uri)) + print("Name: {}".format(glossary.name)) + print("Entry count: {}".format(glossary.entry_count)) + print("Input uri: {}".format(glossary.input_config.gcs_source.input_uri)) for language_code in glossary.language_codes_set.language_codes: - print(u'Language code: {}'.format(language_code)) + print("Language code: {}".format(language_code)) # [END translate_list_glossary_beta] def get_glossary(project_id, glossary_id): # [START translate_get_glossary_beta] from google.cloud import translate_v3beta1 as translate + client = translate.TranslationServiceClient() # project_id = 'YOUR_PROJECT_ID' # glossary_id = 'GLOSSARY_ID' name = client.glossary_path( - project_id, - 'us-central1', # The location of the glossary - glossary_id) + project_id, "us-central1", glossary_id # The location of the glossary + ) response = client.get_glossary(name=name) - print(u'Name: {}'.format(response.name)) - print(u'Language Pair:') - print(u'\tSource Language Code: {}'.format( - response.language_pair.source_language_code)) - print(u'\tTarget Language Code: {}'.format( - response.language_pair.target_language_code)) - print(u'Input Uri: {}'.format( - response.input_config.gcs_source.input_uri)) + print("Name: {}".format(response.name)) + print("Language Pair:") + print( + "\tSource Language Code: {}".format(response.language_pair.source_language_code) + ) + print( + "\tTarget Language Code: {}".format(response.language_pair.target_language_code) + ) + print("Input Uri: {}".format(response.input_config.gcs_source.input_uri)) # [END translate_get_glossary_beta] def delete_glossary(project_id, glossary_id): # [START translate_delete_glossary_beta] from google.cloud import translate_v3beta1 as translate + client = translate.TranslationServiceClient() # project_id = 'YOUR_PROJECT_ID' # glossary_id = 'GLOSSARY_ID' name = client.glossary_path( - project_id, - 'us-central1', # The location of the glossary - glossary_id) + project_id, "us-central1", glossary_id # The location of the glossary + ) operation = client.delete_glossary(name=name) result = operation.result(timeout=90) - print(u'Deleted: {}'.format(result.name)) + print("Deleted: {}".format(result.name)) # [END translate_delete_glossary_beta] def translate_text_with_glossary(project_id, glossary_id, text): # [START translate_translate_text_with_glossary_beta] from google.cloud import translate_v3beta1 as translate + client = translate.TranslationServiceClient() # project_id = 'YOUR_PROJECT_ID' # glossary_id = 'GLOSSARY_ID' # text = 'Text you wish to translate' - location = 'us-central1' # The location of the glossary + location = "us-central1" # The location of the glossary glossary = client.glossary_path( - project_id, - 'us-central1', # The location of the glossary - glossary_id) + project_id, "us-central1", glossary_id # The location of the glossary + ) - glossary_config = translate.types.TranslateTextGlossaryConfig( - glossary=glossary) + glossary_config = translate.types.TranslateTextGlossaryConfig(glossary=glossary) parent = f"projects/{project_id}/locations/{location}" @@ -278,7 +282,7 @@ def translate_text_with_glossary(project_id, glossary_id, text): "mime_type": "text/plain", "source_language_code": "en", "target_language_code": "es", - "glossary_config": glossary_config + "glossary_config": glossary_config, } ) @@ -287,84 +291,92 @@ def translate_text_with_glossary(project_id, glossary_id, text): # [END translate_translate_text_with_glossary_beta] -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) - subparsers = parser.add_subparsers(dest='command') + subparsers = parser.add_subparsers(dest="command") translate_text_parser = subparsers.add_parser( - 'translate-text', help=translate_text.__doc__) - translate_text_parser.add_argument('project_id') - translate_text_parser.add_argument('text') + "translate-text", help=translate_text.__doc__ + ) + translate_text_parser.add_argument("project_id") + translate_text_parser.add_argument("text") batch_translate_text_parser = subparsers.add_parser( - 'batch-translate-text', help=translate_text.__doc__) - batch_translate_text_parser.add_argument('project_id') - batch_translate_text_parser.add_argument('gcs_source') - batch_translate_text_parser.add_argument('gcs_destination') + "batch-translate-text", help=translate_text.__doc__ + ) + batch_translate_text_parser.add_argument("project_id") + batch_translate_text_parser.add_argument("gcs_source") + batch_translate_text_parser.add_argument("gcs_destination") detect_langage_parser = subparsers.add_parser( - 'detect-language', help=detect_language.__doc__) - detect_langage_parser.add_argument('project_id') - detect_langage_parser.add_argument('text') + "detect-language", help=detect_language.__doc__ + ) + detect_langage_parser.add_argument("project_id") + detect_langage_parser.add_argument("text") list_languages_parser = subparsers.add_parser( - 'list-languages', help=list_languages.__doc__) - list_languages_parser.add_argument('project_id') + "list-languages", help=list_languages.__doc__ + ) + list_languages_parser.add_argument("project_id") list_languages_with_target_parser = subparsers.add_parser( - 'list-languages-with-target', help=list_languages_with_target.__doc__) - list_languages_with_target_parser.add_argument('project_id') - list_languages_with_target_parser.add_argument('display_language_code') + "list-languages-with-target", help=list_languages_with_target.__doc__ + ) + list_languages_with_target_parser.add_argument("project_id") + list_languages_with_target_parser.add_argument("display_language_code") create_glossary_parser = subparsers.add_parser( - 'create-glossary', help=create_glossary.__doc__) - create_glossary_parser.add_argument('project_id') - create_glossary_parser.add_argument('glossary_id') + "create-glossary", help=create_glossary.__doc__ + ) + create_glossary_parser.add_argument("project_id") + create_glossary_parser.add_argument("glossary_id") get_glossary_parser = subparsers.add_parser( - 'get-glossary', help=get_glossary.__doc__) - get_glossary_parser.add_argument('project_id') - get_glossary_parser.add_argument('glossary_id') + "get-glossary", help=get_glossary.__doc__ + ) + get_glossary_parser.add_argument("project_id") + get_glossary_parser.add_argument("glossary_id") list_glossary_parser = subparsers.add_parser( - 'list-glossaries', help=list_glossaries.__doc__) - list_glossary_parser.add_argument('project_id') + "list-glossaries", help=list_glossaries.__doc__ + ) + list_glossary_parser.add_argument("project_id") delete_glossary_parser = subparsers.add_parser( - 'delete-glossary', help=delete_glossary.__doc__) - delete_glossary_parser.add_argument('project_id') - delete_glossary_parser.add_argument('glossary_id') + "delete-glossary", help=delete_glossary.__doc__ + ) + delete_glossary_parser.add_argument("project_id") + delete_glossary_parser.add_argument("glossary_id") translate_with_glossary_parser = subparsers.add_parser( - 'translate-with-glossary', help=translate_text_with_glossary.__doc__) - translate_with_glossary_parser.add_argument('project_id') - translate_with_glossary_parser.add_argument('glossary_id') - translate_with_glossary_parser.add_argument('text') + "translate-with-glossary", help=translate_text_with_glossary.__doc__ + ) + translate_with_glossary_parser.add_argument("project_id") + translate_with_glossary_parser.add_argument("glossary_id") + translate_with_glossary_parser.add_argument("text") args = parser.parse_args() - if args.command == 'translate-text': + if args.command == "translate-text": translate_text(args.project_id, args.text) - elif args.command == 'batch-translate-text': - batch_translate_text( - args.project_id, args.gcs_source, args.gcs_destination) - elif args.command == 'detect-language': + elif args.command == "batch-translate-text": + batch_translate_text(args.project_id, args.gcs_source, args.gcs_destination) + elif args.command == "detect-language": detect_language(args.project_id, args.text) - elif args.command == 'list-languages': + elif args.command == "list-languages": list_languages(args.project_id) - elif args.command == 'list-languages-with-target': + elif args.command == "list-languages-with-target": list_languages_with_target(args.project_id, args.display_language_code) - elif args.command == 'create-glossary': + elif args.command == "create-glossary": create_glossary(args.project_id, args.glossary_id) - elif args.command == 'get-glossary': + elif args.command == "get-glossary": get_glossary(args.project_id, args.glossary_id) - elif args.command == 'list-glossaries': + elif args.command == "list-glossaries": list_glossaries(args.project_id) - elif args.command == 'delete-glossary': + elif args.command == "delete-glossary": delete_glossary(args.project_id, args.glossary_id) - elif args.command == 'translate-with-glossary': - translate_text_with_glossary( - args.project_id, args.glossary_id, args.text) + elif args.command == "translate-with-glossary": + translate_text_with_glossary(args.project_id, args.glossary_id, args.text) diff --git a/samples/snippets/beta_snippets_test.py b/samples/snippets/beta_snippets_test.py index a42ab08c..7e0c2dc6 100644 --- a/samples/snippets/beta_snippets_test.py +++ b/samples/snippets/beta_snippets_test.py @@ -22,13 +22,13 @@ import beta_snippets -PROJECT_ID = os.environ['GOOGLE_CLOUD_PROJECT'] +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def bucket(): """Create a temporary bucket to store annotation output.""" - bucket_name = f'tmp-{uuid.uuid4().hex}' + bucket_name = f"tmp-{uuid.uuid4().hex}" storage_client = storage.Client() bucket = storage_client.create_bucket(bucket_name) @@ -37,10 +37,10 @@ def bucket(): bucket.delete(force=True) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def glossary(): """Get the ID of a glossary available to session (do not mutate/delete).""" - glossary_id = 'must-start-with-letters-' + str(uuid.uuid1()) + glossary_id = "must-start-with-letters-" + str(uuid.uuid1()) beta_snippets.create_glossary(PROJECT_ID, glossary_id) yield glossary_id @@ -51,10 +51,10 @@ def glossary(): pass -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def unique_glossary_id(): """Get a unique ID. Attempts to delete glossary with this ID after test.""" - glossary_id = 'must-start-with-letters-' + str(uuid.uuid1()) + glossary_id = "must-start-with-letters-" + str(uuid.uuid1()) yield glossary_id @@ -65,69 +65,69 @@ def unique_glossary_id(): def test_translate_text(capsys): - beta_snippets.translate_text(PROJECT_ID, 'Hello world') + beta_snippets.translate_text(PROJECT_ID, "Hello world") out, _ = capsys.readouterr() - assert 'Translated Text:' in out + assert "Translated Text:" in out @pytest.mark.flaky(max_runs=3, min_passes=1) def test_batch_translate_text(capsys, bucket): beta_snippets.batch_translate_text( PROJECT_ID, - 'gs://cloud-samples-data/translation/text.txt', - 'gs://{}/translation/BATCH_TRANSLATION_OUTPUT/'.format(bucket.name)) + "gs://cloud-samples-data/translation/text.txt", + "gs://{}/translation/BATCH_TRANSLATION_OUTPUT/".format(bucket.name), + ) out, _ = capsys.readouterr() - assert 'Total Characters: 13' in out - assert 'Translated Characters: 13' in out + assert "Total Characters: 13" in out + assert "Translated Characters: 13" in out def test_detect_language(capsys): - beta_snippets.detect_language(PROJECT_ID, u'HΓ¦ sΓ¦ta') + beta_snippets.detect_language(PROJECT_ID, "HΓ¦ sΓ¦ta") out, _ = capsys.readouterr() - assert 'is' in out + assert "is" in out def test_list_languages(capsys): beta_snippets.list_languages(PROJECT_ID) out, _ = capsys.readouterr() - assert 'zh-CN' in out + assert "zh-CN" in out def test_list_languages_with_target(capsys): - beta_snippets.list_languages_with_target(PROJECT_ID, 'is') + beta_snippets.list_languages_with_target(PROJECT_ID, "is") out, _ = capsys.readouterr() - assert u'Language Code: sq' in out - assert u'Display Name: albanska' in out + assert "Language Code: sq" in out + assert "Display Name: albanska" in out @pytest.mark.flaky(max_runs=3, min_passes=1) def test_create_glossary(capsys, unique_glossary_id): beta_snippets.create_glossary(PROJECT_ID, unique_glossary_id) out, _ = capsys.readouterr() - assert 'Created' in out + assert "Created" in out assert unique_glossary_id in out - assert 'gs://cloud-samples-data/translation/glossary.csv' in out + assert "gs://cloud-samples-data/translation/glossary.csv" in out def test_get_glossary(capsys, glossary): beta_snippets.get_glossary(PROJECT_ID, glossary) out, _ = capsys.readouterr() assert glossary in out - assert 'gs://cloud-samples-data/translation/glossary.csv' in out + assert "gs://cloud-samples-data/translation/glossary.csv" in out def test_list_glossary(capsys, glossary): beta_snippets.list_glossaries(PROJECT_ID) out, _ = capsys.readouterr() assert glossary in out - assert 'gs://cloud-samples-data/translation/glossary.csv' in out + assert "gs://cloud-samples-data/translation/glossary.csv" in out def test_translate_text_with_glossary(capsys, glossary): - beta_snippets.translate_text_with_glossary( - PROJECT_ID, glossary, 'account') + beta_snippets.translate_text_with_glossary(PROJECT_ID, glossary, "account") out, _ = capsys.readouterr() - assert 'cuenta' in out + assert "cuenta" in out @pytest.mark.flaky(max_runs=3, min_passes=1) @@ -135,5 +135,5 @@ def test_delete_glossary(capsys, unique_glossary_id): beta_snippets.create_glossary(PROJECT_ID, unique_glossary_id) beta_snippets.delete_glossary(PROJECT_ID, unique_glossary_id) out, _ = capsys.readouterr() - assert 'us-central1' in out + assert "us-central1" in out assert unique_glossary_id in out diff --git a/samples/snippets/hybrid_glossaries/hybrid_tutorial.py b/samples/snippets/hybrid_glossaries/hybrid_tutorial.py index a9ea9328..c98bdd0e 100644 --- a/samples/snippets/hybrid_glossaries/hybrid_tutorial.py +++ b/samples/snippets/hybrid_glossaries/hybrid_tutorial.py @@ -23,12 +23,13 @@ from google.cloud import texttospeech from google.cloud import translate_v3beta1 as translate from google.cloud import vision + # [END translate_hybrid_imports] # [START translate_hybrid_project_id] # extract GCP project id -PROJECT_ID = os.environ['GOOGLE_CLOUD_PROJECT'] +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] # [END translate_hybrid_project_id] @@ -47,7 +48,7 @@ def pic_to_text(infile): client = vision.ImageAnnotatorClient() # Opens the input image file - with io.open(infile, 'rb') as image_file: + with io.open(infile, "rb") as image_file: content = image_file.read() image = vision.types.Image(content=content) @@ -81,29 +82,24 @@ def create_glossary(languages, project_id, glossary_name, glossary_uri): client = translate.TranslationServiceClient() # Designates the data center location that you want to use - location = 'us-central1' + location = "us-central1" # Set glossary resource name - name = client.glossary_path( - project_id, - location, - glossary_name) + name = client.glossary_path(project_id, location, glossary_name) # Set language codes language_codes_set = translate.types.Glossary.LanguageCodesSet( - language_codes=languages) + language_codes=languages + ) - gcs_source = translate.types.GcsSource( - input_uri=glossary_uri) + gcs_source = translate.types.GcsSource(input_uri=glossary_uri) - input_config = translate.types.GlossaryInputConfig( - gcs_source=gcs_source) + input_config = translate.types.GlossaryInputConfig(gcs_source=gcs_source) # Set glossary resource information glossary = translate.types.Glossary( - name=name, - language_codes_set=language_codes_set, - input_config=input_config) + name=name, language_codes_set=language_codes_set, input_config=input_config + ) parent = f"projects/{project_id}/locations/{location}" @@ -113,16 +109,20 @@ def create_glossary(languages, project_id, glossary_name, glossary_uri): try: operation = client.create_glossary(parent=parent, glossary=glossary) operation.result(timeout=90) - print('Created glossary ' + glossary_name + '.') + print("Created glossary " + glossary_name + ".") except AlreadyExists: - print('The glossary ' + glossary_name + - ' already exists. No new glossary was created.') + print( + "The glossary " + + glossary_name + + " already exists. No new glossary was created." + ) # [END translate_hybrid_create_glossary] # [START translate_hybrid_translate] -def translate_text(text, source_language_code, target_language_code, - project_id, glossary_name): +def translate_text( + text, source_language_code, target_language_code, project_id, glossary_name +): """Translates text to a given language using a glossary ARGS @@ -141,15 +141,11 @@ def translate_text(text, source_language_code, target_language_code, client = translate.TranslationServiceClient() # Designates the data center location that you want to use - location = 'us-central1' + location = "us-central1" - glossary = client.glossary_path( - project_id, - location, - glossary_name) + glossary = client.glossary_path(project_id, location, glossary_name) - glossary_config = translate.types.TranslateTextGlossaryConfig( - glossary=glossary) + glossary_config = translate.types.TranslateTextGlossaryConfig(glossary=glossary) parent = f"projects/{project_id}/locations/{location}" @@ -160,7 +156,7 @@ def translate_text(text, source_language_code, target_language_code, "mime_type": "text/plain", # mime types: text/plain, text/html "source_language_code": source_language_code, "target_language_code": target_language_code, - "glossary_config": glossary_config + "glossary_config": glossary_config, } ) @@ -190,8 +186,9 @@ def text_to_speech(text, outfile): # Convert plaintext to SSML in order to wait two seconds # between each line in synthetic speech - ssml = '{}'.format( - escaped_lines.replace('\n', '\n')) + ssml = "{}".format( + escaped_lines.replace("\n", '\n') + ) # Instantiates a client client = texttospeech.TextToSpeechClient() @@ -202,28 +199,27 @@ def text_to_speech(text, outfile): # Builds the voice request, selects the language code ("en-US") and # the SSML voice gender ("MALE") voice = texttospeech.VoiceSelectionParams( - language_code='en-US', - ssml_gender=texttospeech.SsmlVoiceGender.MALE) + language_code="en-US", ssml_gender=texttospeech.SsmlVoiceGender.MALE + ) # Selects the type of audio file to return audio_config = texttospeech.AudioConfig( - audio_encoding=texttospeech.AudioEncoding.MP3) + audio_encoding=texttospeech.AudioEncoding.MP3 + ) # Performs the text-to-speech request on the text input with the selected # voice parameters and audio file type request = texttospeech.SynthesizeSpeechRequest( - input=synthesis_input, - voice=voice, - audio_config=audio_config + input=synthesis_input, voice=voice, audio_config=audio_config ) response = client.synthesize_speech(request=request) # Writes the synthetic audio to the output file. - with open(outfile, 'wb') as out: + with open(outfile, "wb") as out: out.write(response.audio_content) - print('Audio content written to file ' + outfile) + print("Audio content written to file " + outfile) # [END translate_hybrid_tts] @@ -231,30 +227,31 @@ def text_to_speech(text, outfile): def main(): # Photo from which to extract text - infile = 'resources/example.png' + infile = "resources/example.png" # Name of file that will hold synthetic speech - outfile = 'resources/example.mp3' + outfile = "resources/example.mp3" # Defines the languages in the glossary # This list must match the languages in the glossary # Here, the glossary includes French and English - glossary_langs = ['fr', 'en'] + glossary_langs = ["fr", "en"] # Name that will be assigned to your project's glossary resource - glossary_name = 'bistro-glossary' + glossary_name = "bistro-glossary" # uri of .csv file uploaded to Cloud Storage - glossary_uri = 'gs://cloud-samples-data/translation/bistro_glossary.csv' + glossary_uri = "gs://cloud-samples-data/translation/bistro_glossary.csv" create_glossary(glossary_langs, PROJECT_ID, glossary_name, glossary_uri) # photo -> detected text text_to_translate = pic_to_text(infile) # detected text -> translated text - text_to_speak = translate_text(text_to_translate, 'fr', 'en', - PROJECT_ID, glossary_name) + text_to_speak = translate_text( + text_to_translate, "fr", "en", PROJECT_ID, glossary_name + ) # translated text -> synthetic audio text_to_speech(text_to_speak, outfile) # [END translate_hybrid_integration] -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/samples/snippets/hybrid_glossaries/hybrid_tutorial_test.py b/samples/snippets/hybrid_glossaries/hybrid_tutorial_test.py index 56439688..eaccf7f2 100644 --- a/samples/snippets/hybrid_glossaries/hybrid_tutorial_test.py +++ b/samples/snippets/hybrid_glossaries/hybrid_tutorial_test.py @@ -22,7 +22,7 @@ from hybrid_tutorial import translate_text -PROJECT_ID = os.environ['GOOGLE_CLOUD_PROJECT'] +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] # VISION TESTS @@ -40,10 +40,10 @@ def test_vision_standard_format(capsys): def test_vision_non_standard_format(): # Generate text - text = pic_to_text('resources/non_standard_format.png') + text = pic_to_text("resources/non_standard_format.png") # Read expected text - with open('resources/non_standard_format.txt') as f: + with open("resources/non_standard_format.txt") as f: expected_text = f.read() assert text == expected_text @@ -53,16 +53,15 @@ def test_vision_non_standard_format(): def test_create_and_delete_glossary(): - sys.path.insert(1, '../') + sys.path.insert(1, "../") from beta_snippets import delete_glossary - languages = ['fr', 'en'] - glossary_name = f'test-glossary-{uuid.uuid4()}' - glossary_uri = 'gs://cloud-samples-data/translation/bistro_glossary.csv' + languages = ["fr", "en"] + glossary_name = f"test-glossary-{uuid.uuid4()}" + glossary_uri = "gs://cloud-samples-data/translation/bistro_glossary.csv" # create_glossary will raise an exception if creation fails - create_glossary(languages, PROJECT_ID, glossary_name, - glossary_uri) + create_glossary(languages, PROJECT_ID, glossary_name, glossary_uri) # Delete glossary so that future tests will pass # delete_glossary will raise an exception if deletion fails @@ -71,21 +70,19 @@ def test_create_and_delete_glossary(): def test_translate_standard(): - expected_text = 'Hello' + expected_text = "Hello" - text = translate_text('Bonjour', 'fr', 'en', PROJECT_ID, - 'bistro-glossary') + text = translate_text("Bonjour", "fr", "en", PROJECT_ID, "bistro-glossary") assert text == expected_text def test_translate_glossary(): - expected_text = 'I eat goat cheese' - input_text = 'Je mange du chevre' + expected_text = "I eat goat cheese" + input_text = "Je mange du chevre" - text = translate_text(input_text, 'fr', 'en', PROJECT_ID, - 'bistro-glossary') + text = translate_text(input_text, "fr", "en", PROJECT_ID, "bistro-glossary") assert text == expected_text @@ -94,10 +91,10 @@ def test_translate_glossary(): def test_tts_standard(capsys): - outfile = 'resources/test_standard_text.mp3' - textfile = 'resources/standard_format.txt' + outfile = "resources/test_standard_text.mp3" + textfile = "resources/standard_format.txt" - with open(textfile, 'r') as f: + with open(textfile, "r") as f: text = f.read() text_to_speech(text, outfile) @@ -107,7 +104,7 @@ def test_tts_standard(capsys): out, err = capsys.readouterr() # Assert success message printed - assert 'Audio content written to file ' + outfile in out + assert "Audio content written to file " + outfile in out # Delete test file os.remove(outfile) diff --git a/samples/snippets/hybrid_glossaries/noxfile.py b/samples/snippets/hybrid_glossaries/noxfile.py index ba55d7ce..5660f08b 100644 --- a/samples/snippets/hybrid_glossaries/noxfile.py +++ b/samples/snippets/hybrid_glossaries/noxfile.py @@ -37,24 +37,22 @@ TEST_CONFIG = { # You can opt out from the test for specific Python versions. - 'ignored_versions': ["2.7"], - + "ignored_versions": ["2.7"], # An envvar key for determining the project id to use. Change it # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a # build specific Cloud project. You can also use your own string # to use your own Cloud project. - 'gcloud_project_env': 'GOOGLE_CLOUD_PROJECT', + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any # secrets here. These values will override predefined values. - 'envs': {}, + "envs": {}, } try: # Ensure we can import noxfile_config in the project's directory. - sys.path.append('.') + sys.path.append(".") from noxfile_config import TEST_CONFIG_OVERRIDE except ImportError as e: print("No user noxfile_config found: detail: {}".format(e)) @@ -69,12 +67,12 @@ def get_pytest_env_vars(): ret = {} # Override the GCLOUD_PROJECT and the alias. - env_key = TEST_CONFIG['gcloud_project_env'] + env_key = TEST_CONFIG["gcloud_project_env"] # This should error out if not set. - ret['GOOGLE_CLOUD_PROJECT'] = os.environ[env_key] + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] # Apply user supplied envs. - ret.update(TEST_CONFIG['envs']) + ret.update(TEST_CONFIG["envs"]) return ret @@ -83,7 +81,7 @@ def get_pytest_env_vars(): ALL_VERSIONS = ["2.7", "3.6", "3.7", "3.8"] # Any default versions that should be ignored. -IGNORED_VERSIONS = TEST_CONFIG['ignored_versions'] +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) @@ -138,7 +136,7 @@ def lint(session): args = FLAKE8_COMMON_ARGS + [ "--application-import-names", ",".join(local_names), - "." + ".", ] session.run("flake8", *args) @@ -182,9 +180,9 @@ def py(session): if session.python in TESTED_VERSIONS: _session_tests(session) else: - session.skip("SKIPPED: {} tests are disabled for this sample.".format( - session.python - )) + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) # diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index ba55d7ce..5660f08b 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -37,24 +37,22 @@ TEST_CONFIG = { # You can opt out from the test for specific Python versions. - 'ignored_versions': ["2.7"], - + "ignored_versions": ["2.7"], # An envvar key for determining the project id to use. Change it # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a # build specific Cloud project. You can also use your own string # to use your own Cloud project. - 'gcloud_project_env': 'GOOGLE_CLOUD_PROJECT', + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', - # A dictionary you want to inject into your test. Don't put any # secrets here. These values will override predefined values. - 'envs': {}, + "envs": {}, } try: # Ensure we can import noxfile_config in the project's directory. - sys.path.append('.') + sys.path.append(".") from noxfile_config import TEST_CONFIG_OVERRIDE except ImportError as e: print("No user noxfile_config found: detail: {}".format(e)) @@ -69,12 +67,12 @@ def get_pytest_env_vars(): ret = {} # Override the GCLOUD_PROJECT and the alias. - env_key = TEST_CONFIG['gcloud_project_env'] + env_key = TEST_CONFIG["gcloud_project_env"] # This should error out if not set. - ret['GOOGLE_CLOUD_PROJECT'] = os.environ[env_key] + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] # Apply user supplied envs. - ret.update(TEST_CONFIG['envs']) + ret.update(TEST_CONFIG["envs"]) return ret @@ -83,7 +81,7 @@ def get_pytest_env_vars(): ALL_VERSIONS = ["2.7", "3.6", "3.7", "3.8"] # Any default versions that should be ignored. -IGNORED_VERSIONS = TEST_CONFIG['ignored_versions'] +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) @@ -138,7 +136,7 @@ def lint(session): args = FLAKE8_COMMON_ARGS + [ "--application-import-names", ",".join(local_names), - "." + ".", ] session.run("flake8", *args) @@ -182,9 +180,9 @@ def py(session): if session.python in TESTED_VERSIONS: _session_tests(session) else: - session.skip("SKIPPED: {} tests are disabled for this sample.".format( - session.python - )) + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) # diff --git a/samples/snippets/quickstart.py b/samples/snippets/quickstart.py index 888b6691..9fc4027a 100644 --- a/samples/snippets/quickstart.py +++ b/samples/snippets/quickstart.py @@ -24,19 +24,17 @@ def run_quickstart(): translate_client = translate.Client() # The text to translate - text = u'Hello, world!' + text = u"Hello, world!" # The target language - target = 'ru' + target = "ru" # Translates some text into Russian - translation = translate_client.translate( - text, - target_language=target) + translation = translate_client.translate(text, target_language=target) - print(u'Text: {}'.format(text)) - print(u'Translation: {}'.format(translation['translatedText'])) + print(u"Text: {}".format(text)) + print(u"Translation: {}".format(translation["translatedText"])) # [END translate_quickstart] -if __name__ == '__main__': +if __name__ == "__main__": run_quickstart() diff --git a/samples/snippets/quickstart_test.py b/samples/snippets/quickstart_test.py index 8def7f3e..e4dc4886 100644 --- a/samples/snippets/quickstart_test.py +++ b/samples/snippets/quickstart_test.py @@ -20,4 +20,4 @@ def test_quickstart(capsys): quickstart.run_quickstart() out, _ = capsys.readouterr() - assert u'Translation' in out + assert u"Translation" in out diff --git a/samples/snippets/snippets.py b/samples/snippets/snippets.py index 24b45716..41d64c32 100644 --- a/samples/snippets/snippets.py +++ b/samples/snippets/snippets.py @@ -30,15 +30,16 @@ def detect_language(text): # [START translate_detect_language] """Detects the text's language.""" from google.cloud import translate_v2 as translate + translate_client = translate.Client() # Text can also be a sequence of strings, in which case this method # will return a sequence of results for each text. result = translate_client.detect_language(text) - print('Text: {}'.format(text)) - print('Confidence: {}'.format(result['confidence'])) - print('Language: {}'.format(result['language'])) + print("Text: {}".format(text)) + print("Confidence: {}".format(result["confidence"])) + print("Language: {}".format(result["language"])) # [END translate_detect_language] @@ -46,12 +47,13 @@ def list_languages(): # [START translate_list_codes] """Lists all available languages.""" from google.cloud import translate_v2 as translate + translate_client = translate.Client() results = translate_client.get_languages() for language in results: - print(u'{name} ({language})'.format(**language)) + print(u"{name} ({language})".format(**language)) # [END translate_list_codes] @@ -63,16 +65,17 @@ def list_languages_with_target(target): See https://g.co/cloud/translate/v2/translate-reference#supported_languages """ from google.cloud import translate_v2 as translate + translate_client = translate.Client() results = translate_client.get_languages(target_language=target) for language in results: - print(u'{name} ({language})'.format(**language)) + print(u"{name} ({language})".format(**language)) # [END translate_list_language_names] -def translate_text_with_model(target, text, model='nmt'): +def translate_text_with_model(target, text, model="nmt"): # [START translate_text_with_model] """Translates text into the target language. @@ -82,20 +85,19 @@ def translate_text_with_model(target, text, model='nmt'): See https://g.co/cloud/translate/v2/translate-reference#supported_languages """ from google.cloud import translate_v2 as translate + translate_client = translate.Client() if isinstance(text, six.binary_type): - text = text.decode('utf-8') + text = text.decode("utf-8") # Text can also be a sequence of strings, in which case this method # will return a sequence of results for each text. - result = translate_client.translate( - text, target_language=target, model=model) + result = translate_client.translate(text, target_language=target, model=model) - print(u'Text: {}'.format(result['input'])) - print(u'Translation: {}'.format(result['translatedText'])) - print(u'Detected source language: {}'.format( - result['detectedSourceLanguage'])) + print(u"Text: {}".format(result["input"])) + print(u"Translation: {}".format(result["translatedText"])) + print(u"Detected source language: {}".format(result["detectedSourceLanguage"])) # [END translate_text_with_model] @@ -107,52 +109,55 @@ def translate_text(target, text): See https://g.co/cloud/translate/v2/translate-reference#supported_languages """ from google.cloud import translate_v2 as translate + translate_client = translate.Client() if isinstance(text, six.binary_type): - text = text.decode('utf-8') + text = text.decode("utf-8") # Text can also be a sequence of strings, in which case this method # will return a sequence of results for each text. - result = translate_client.translate( - text, target_language=target) + result = translate_client.translate(text, target_language=target) - print(u'Text: {}'.format(result['input'])) - print(u'Translation: {}'.format(result['translatedText'])) - print(u'Detected source language: {}'.format( - result['detectedSourceLanguage'])) + print(u"Text: {}".format(result["input"])) + print(u"Translation: {}".format(result["translatedText"])) + print(u"Detected source language: {}".format(result["detectedSourceLanguage"])) # [END translate_translate_text] -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) - subparsers = parser.add_subparsers(dest='command') + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + subparsers = parser.add_subparsers(dest="command") detect_langage_parser = subparsers.add_parser( - 'detect-language', help=detect_language.__doc__) - detect_langage_parser.add_argument('text') + "detect-language", help=detect_language.__doc__ + ) + detect_langage_parser.add_argument("text") list_languages_parser = subparsers.add_parser( - 'list-languages', help=list_languages.__doc__) + "list-languages", help=list_languages.__doc__ + ) list_languages_with_target_parser = subparsers.add_parser( - 'list-languages-with-target', help=list_languages_with_target.__doc__) - list_languages_with_target_parser.add_argument('target') + "list-languages-with-target", help=list_languages_with_target.__doc__ + ) + list_languages_with_target_parser.add_argument("target") translate_text_parser = subparsers.add_parser( - 'translate-text', help=translate_text.__doc__) - translate_text_parser.add_argument('target') - translate_text_parser.add_argument('text') + "translate-text", help=translate_text.__doc__ + ) + translate_text_parser.add_argument("target") + translate_text_parser.add_argument("text") args = parser.parse_args() - if args.command == 'detect-language': + if args.command == "detect-language": detect_language(args.text) - elif args.command == 'list-languages': + elif args.command == "list-languages": list_languages() - elif args.command == 'list-languages-with-target': + elif args.command == "list-languages-with-target": list_languages_with_target(args.target) - elif args.command == 'translate-text': + elif args.command == "translate-text": translate_text(args.target, args.text) diff --git a/samples/snippets/snippets_test.py b/samples/snippets/snippets_test.py index ab489dfa..b5fe362c 100644 --- a/samples/snippets/snippets_test.py +++ b/samples/snippets/snippets_test.py @@ -19,31 +19,31 @@ def test_detect_language(capsys): - snippets.detect_language('HΓ¦ sΓ¦ta') + snippets.detect_language("HΓ¦ sΓ¦ta") out, _ = capsys.readouterr() - assert 'is' in out + assert "is" in out def test_list_languages(capsys): snippets.list_languages() out, _ = capsys.readouterr() - assert 'Icelandic (is)' in out + assert "Icelandic (is)" in out def test_list_languages_with_target(capsys): - snippets.list_languages_with_target('is') + snippets.list_languages_with_target("is") out, _ = capsys.readouterr() - assert u'Γ­slenska (is)' in out + assert u"Γ­slenska (is)" in out def test_translate_text(capsys): - snippets.translate_text('is', 'Hello world') + snippets.translate_text("is", "Hello world") out, _ = capsys.readouterr() - assert u'HallΓ³ heimur' in out + assert u"HallΓ³ heimur" in out def test_translate_utf8(capsys): - text = u'νŒŒμΈμ• ν”Œ 13개' - snippets.translate_text('en', text) + text = u"νŒŒμΈμ• ν”Œ 13개" + snippets.translate_text("en", text) out, _ = capsys.readouterr() - assert u'13 pineapples' in out + assert u"13 pineapples" in out diff --git a/samples/snippets/translate_v3_batch_translate_text.py b/samples/snippets/translate_v3_batch_translate_text.py index 487b0965..0f4161c9 100644 --- a/samples/snippets/translate_v3_batch_translate_text.py +++ b/samples/snippets/translate_v3_batch_translate_text.py @@ -17,10 +17,10 @@ def batch_translate_text( - input_uri="gs://YOUR_BUCKET_ID/path/to/your/file.txt", - output_uri="gs://YOUR_BUCKET_ID/path/to/save/results/", - project_id="YOUR_PROJECT_ID", - timeout=180, + input_uri="gs://YOUR_BUCKET_ID/path/to/your/file.txt", + output_uri="gs://YOUR_BUCKET_ID/path/to/save/results/", + project_id="YOUR_PROJECT_ID", + timeout=180, ): """Translates a batch of texts on GCS and stores the result in a GCS location.""" @@ -32,7 +32,7 @@ def batch_translate_text( input_configs_element = { "gcs_source": gcs_source, - "mime_type": "text/plain" # Can be "text/plain" or "text/html". + "mime_type": "text/plain", # Can be "text/plain" or "text/html". } gcs_destination = {"output_uri_prefix": output_uri} output_config = {"gcs_destination": gcs_destination} @@ -45,15 +45,15 @@ def batch_translate_text( "source_language_code": "en", "target_language_codes": ["ja"], # Up to 10 language codes here. "input_configs": [input_configs_element], - "output_config": output_config + "output_config": output_config, } ) - print(u"Waiting for operation to complete...") + print("Waiting for operation to complete...") response = operation.result(timeout) - print(u"Total Characters: {}".format(response.total_characters)) - print(u"Translated Characters: {}".format(response.translated_characters)) + print("Total Characters: {}".format(response.total_characters)) + print("Translated Characters: {}".format(response.translated_characters)) # [END translate_v3_batch_translate_text] diff --git a/samples/snippets/translate_v3_batch_translate_text_test.py b/samples/snippets/translate_v3_batch_translate_text_test.py index f604a3e1..8629d475 100644 --- a/samples/snippets/translate_v3_batch_translate_text_test.py +++ b/samples/snippets/translate_v3_batch_translate_text_test.py @@ -42,7 +42,7 @@ def test_batch_translate_text(capsys, bucket): "gs://cloud-samples-data/translation/text.txt", "gs://{}/translation/BATCH_TRANSLATION_OUTPUT/".format(bucket.name), PROJECT_ID, - timeout=240 + timeout=240, ) out, _ = capsys.readouterr() assert "Total Characters" in out diff --git a/samples/snippets/translate_v3_batch_translate_text_with_glossary.py b/samples/snippets/translate_v3_batch_translate_text_with_glossary.py index e9c4a7c2..62500342 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_glossary.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_glossary.py @@ -18,11 +18,11 @@ def batch_translate_text_with_glossary( - input_uri="gs://YOUR_BUCKET_ID/path/to/your/file.txt", - output_uri="gs://YOUR_BUCKET_ID/path/to/save/results/", - project_id="YOUR_PROJECT_ID", - glossary_id="YOUR_GLOSSARY_ID", - timeout=180, + input_uri="gs://YOUR_BUCKET_ID/path/to/your/file.txt", + output_uri="gs://YOUR_BUCKET_ID/path/to/save/results/", + project_id="YOUR_PROJECT_ID", + glossary_id="YOUR_GLOSSARY_ID", + timeout=180, ): """Translates a batch of texts on GCS and stores the result in a GCS location. Glossary is applied for translation.""" @@ -37,7 +37,7 @@ def batch_translate_text_with_glossary( input_configs_element = { "gcs_source": gcs_source, - "mime_type": "text/plain" # Can be "text/plain" or "text/html". + "mime_type": "text/plain", # Can be "text/plain" or "text/html". } gcs_destination = {"output_uri_prefix": output_uri} output_config = {"gcs_destination": gcs_destination} @@ -50,9 +50,7 @@ def batch_translate_text_with_glossary( project_id, "us-central1", glossary_id # The location of the glossary ) - glossary_config = translate.TranslateTextGlossaryConfig( - glossary=glossary_path - ) + glossary_config = translate.TranslateTextGlossaryConfig(glossary=glossary_path) glossaries = {"ja": glossary_config} # target lang as key @@ -63,15 +61,15 @@ def batch_translate_text_with_glossary( "target_language_codes": ["ja"], # Up to 10 language codes here. "input_configs": [input_configs_element], "glossaries": glossaries, - "output_config": output_config + "output_config": output_config, } ) - print(u"Waiting for operation to complete...") + print("Waiting for operation to complete...") response = operation.result(timeout) - print(u"Total Characters: {}".format(response.total_characters)) - print(u"Translated Characters: {}".format(response.translated_characters)) + print("Total Characters: {}".format(response.total_characters)) + print("Translated Characters: {}".format(response.translated_characters)) # [END translate_v3_batch_translate_text_with_glossary] diff --git a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py index acb53fcd..e9a6905e 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py @@ -27,6 +27,7 @@ # [START translate_v3_batch_translate_text_with_glossary_and_model] from google.cloud import translate + def sample_batch_translate_text_with_glossary_and_model( input_uri, output_uri, @@ -35,7 +36,7 @@ def sample_batch_translate_text_with_glossary_and_model( target_language, source_language, model_id, - glossary_id + glossary_id, ): """ Batch translate text with Glossary and Translation model @@ -61,35 +62,37 @@ def sample_batch_translate_text_with_glossary_and_model( input_configs = [input_configs_element] gcs_destination = {"output_uri_prefix": output_uri} output_config = {"gcs_destination": gcs_destination} - parent = f"projects/{project_id}/locations/location" - model_path = 'projects/{}/locations/{}/models/{}'.format(project_id, 'us-central1', model_id) + parent = f"projects/{project_id}/locations/{location}" + model_path = "projects/{}/locations/{}/models/{}".format( + project_id, "us-central1", model_id + ) models = {target_language: model_path} glossary_path = client.glossary_path( - project_id, - 'us-central1', # The location of the glossary - glossary_id) - - glossary_config = translate.types.TranslateTextGlossaryConfig( - glossary=glossary_path) - glossaries = {"ja": glossary_config} #target lang as key + project_id, "us-central1", glossary_id # The location of the glossary + ) + + glossary_config = translate.TranslateTextGlossaryConfig(glossary=glossary_path) + glossaries = {"ja": glossary_config} # target lang as key operation = client.batch_translate_text( - parent=parent, - source_language_code=source_language, - target_language_codes=target_language_codes, - input_configs=input_configs, - output_config=output_config, - models=models, - glossaries=glossaries + request={ + "parent": parent, + "source_language_code": "en", + "target_language_codes": target_language_codes, + "input_configs": input_configs, + "output_config": output_config, + "models": models, + "glossaries": glossaries, + } ) - print(u"Waiting for operation to complete...") + print("Waiting for operation to complete...") response = operation.result() # Display the translation for each input text provided - print(u"Total Characters: {}".format(response.total_characters)) - print(u"Translated Characters: {}".format(response.translated_characters)) + print("Total Characters: {}".format(response.total_characters)) + print("Translated Characters: {}".format(response.translated_characters)) # [END translate_v3_batch_translate_text_with_glossary_and_model] @@ -109,15 +112,9 @@ def main(): parser.add_argument("--location", type=str, default="us-central1") parser.add_argument("--target_language", type=str, default="en") parser.add_argument("--source_language", type=str, default="de") + parser.add_argument("--model_id", type=str, default="{your-model-id}") parser.add_argument( - "--model_id", - type=str, - default="{your-model-id}" - ) - parser.add_argument( - "--glossary_id", - type=str, - default="[YOUR_GLOSSARY_ID]", + "--glossary_id", type=str, default="[YOUR_GLOSSARY_ID]", ) args = parser.parse_args() @@ -129,9 +126,9 @@ def main(): args.target_language, args.source_language, args.model_id, - args.glossary_id + args.glossary_id, ) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py index 563c9f74..a750f0df 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py @@ -13,25 +13,26 @@ # limitations under the License. import os -import pytest import uuid + +from google.cloud import storage +import pytest + import translate_v3_batch_translate_text_with_glossary_and_model import translate_v3_create_glossary import translate_v3_delete_glossary -from google.cloud import storage -PROJECT_ID = os.environ['GOOGLE_CLOUD_PROJECT'] -GLOSSARY_INPUT_URI = 'gs://cloud-samples-data/translation/glossary_ja.csv' -MODEL_ID = 'TRL3128559826197068699' +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] +GLOSSARY_INPUT_URI = "gs://cloud-samples-data/translation/glossary_ja.csv" +MODEL_ID = "TRL3128559826197068699" + -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def glossary(): """Get the ID of a glossary available to session (do not mutate/delete).""" - glossary_id = 'must-start-with-letters-' + str(uuid.uuid1()) - translate_v3_create_glossary.sample_create_glossary( - PROJECT_ID, - GLOSSARY_INPUT_URI, - glossary_id + glossary_id = "must-start-with-letters-" + str(uuid.uuid1()) + translate_v3_create_glossary.create_glossary( + project_id=PROJECT_ID, input_uri=GLOSSARY_INPUT_URI, glossary_id=glossary_id ) yield glossary_id @@ -40,8 +41,9 @@ def glossary(): translate_v3_delete_glossary.sample_delete_glossary(PROJECT_ID, glossary_id) except Exception: pass - -@pytest.fixture(scope='function') + + +@pytest.fixture(scope="function") def bucket(): """Create a temporary bucket to store annotation output.""" bucket_name = "mike-test-delete-" + str(uuid.uuid1()) @@ -52,21 +54,22 @@ def bucket(): bucket.delete(force=True) + def test_batch_translate_text_with_glossary_and_model(capsys, bucket, glossary): translate_v3_batch_translate_text_with_glossary_and_model.sample_batch_translate_text_with_glossary_and_model( - 'gs://cloud-samples-data/translation/text_with_custom_model_and_glossary.txt', - 'gs://{}/translation/BATCH_TRANSLATION_OUTPUT/'.format(bucket.name), + "gs://cloud-samples-data/translation/text_with_custom_model_and_glossary.txt", + "gs://{}/translation/BATCH_TRANSLATION_OUTPUT/".format(bucket.name), PROJECT_ID, - 'us-central1', - 'ja', - 'en', + "us-central1", + "ja", + "en", MODEL_ID, - glossary - ) - + glossary, + ) + out, _ = capsys.readouterr() - assert 'Total Characters: 25' in out - #TODO: find a way to make sure it translates correctly + assert "Total Characters: 25" in out + # TODO: find a way to make sure it translates correctly # SHOULD NOT BE - Google NMT model -> γγ‚Œγ―γ—γΎγ›γ‚“γ€‚ζ¬Ίception" # literal: "γγ‚Œγ―γγ†γ " # custom model - # literal: "欺く" # glossary \ No newline at end of file + # literal: "欺く" # glossary diff --git a/samples/snippets/translate_v3_batch_translate_text_with_glossary_test.py b/samples/snippets/translate_v3_batch_translate_text_with_glossary_test.py index 23aa6691..726a8e0c 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_glossary_test.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_glossary_test.py @@ -46,18 +46,18 @@ def glossary(): ) def delete_glossary(): try: - translate_v3_delete_glossary.delete_glossary( - PROJECT_ID, glossary_id) + translate_v3_delete_glossary.delete_glossary(PROJECT_ID, glossary_id) except NotFound as e: # Ignoring this case. print("Got NotFound, detail: {}".format(str(e))) + delete_glossary() @pytest.fixture(scope="function") def bucket(): """Create a temporary bucket to store annotation output.""" - bucket_name = f'tmp-{uuid.uuid4().hex}' + bucket_name = f"tmp-{uuid.uuid4().hex}" storage_client = storage.Client() bucket = storage_client.create_bucket(bucket_name) @@ -73,7 +73,7 @@ def test_batch_translate_text_with_glossary(capsys, bucket, glossary): "gs://{}/translation/BATCH_TRANSLATION_OUTPUT/".format(bucket.name), PROJECT_ID, glossary, - 240 + 240, ) out, _ = capsys.readouterr() diff --git a/samples/snippets/translate_v3_batch_translate_text_with_model.py b/samples/snippets/translate_v3_batch_translate_text_with_model.py index 010f7f93..07d967d7 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_model.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_model.py @@ -34,11 +34,11 @@ def batch_translate_text_with_model( input_configs_element = { "gcs_source": gcs_source, - "mime_type": "text/plain" # Can be "text/plain" or "text/html". + "mime_type": "text/plain", # Can be "text/plain" or "text/html". } gcs_destination = {"output_uri_prefix": output_uri} output_config = {"gcs_destination": gcs_destination} - parent = client.location_path(project_id, location) + parent = f"projects/{project_id}/locations/{location}" model_path = "projects/{}/locations/{}/models/{}".format( project_id, location, model_id # The location of AutoML model. @@ -48,20 +48,22 @@ def batch_translate_text_with_model( models = {"ja": model_path} # takes a target lang as key. operation = client.batch_translate_text( - parent=parent, - source_language_code="en", - target_language_codes=["ja"], # Up to 10 language codes here. - input_configs=[input_configs_element], - output_config=output_config, - models=models, + request={ + "parent": parent, + "source_language_code": "en", + "target_language_codes": ["ja"], # Up to 10 language codes here. + "input_configs": [input_configs_element], + "output_config": output_config, + "models": models, + } ) - print(u"Waiting for operation to complete...") + print("Waiting for operation to complete...") response = operation.result() # Display the translation for each input text provided. - print(u"Total Characters: {}".format(response.total_characters)) - print(u"Translated Characters: {}".format(response.translated_characters)) + print("Total Characters: {}".format(response.total_characters)) + print("Translated Characters: {}".format(response.translated_characters)) # [END translate_v3_batch_translate_text_with_model] diff --git a/samples/snippets/translate_v3_batch_translate_text_with_model_test.py b/samples/snippets/translate_v3_batch_translate_text_with_model_test.py index 4d0def04..f6ad1007 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_model_test.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_model_test.py @@ -28,7 +28,7 @@ @pytest.fixture(scope="function") def bucket(): """Create a temporary bucket to store annotation output.""" - bucket_name = f'tmp-{uuid.uuid4().hex}' + bucket_name = f"tmp-{uuid.uuid4().hex}" storage_client = storage.Client() bucket = storage_client.create_bucket(bucket_name) diff --git a/samples/snippets/translate_v3_create_glossary.py b/samples/snippets/translate_v3_create_glossary.py index 2955acaa..b2fd3863 100644 --- a/samples/snippets/translate_v3_create_glossary.py +++ b/samples/snippets/translate_v3_create_glossary.py @@ -17,10 +17,10 @@ def create_glossary( - project_id="YOUR_PROJECT_ID", - input_uri="YOUR_INPUT_URI", - glossary_id="YOUR_GLOSSARY_ID", - timeout=180, + project_id="YOUR_PROJECT_ID", + input_uri="YOUR_INPUT_URI", + glossary_id="YOUR_GLOSSARY_ID", + timeout=180, ): """ Create a equivalent term sets glossary. Glossary can be words or diff --git a/samples/snippets/translate_v3_create_glossary_test.py b/samples/snippets/translate_v3_create_glossary_test.py index a24f461e..3e4b61cf 100644 --- a/samples/snippets/translate_v3_create_glossary_test.py +++ b/samples/snippets/translate_v3_create_glossary_test.py @@ -46,9 +46,9 @@ def test_create_glossary(capsys): ) def delete_glossary(): try: - translate_v3_delete_glossary.delete_glossary( - PROJECT_ID, glossary_id) + translate_v3_delete_glossary.delete_glossary(PROJECT_ID, glossary_id) except NotFound as e: # Ignoring this case. print("Got NotFound, detail: {}".format(str(e))) + delete_glossary() diff --git a/samples/snippets/translate_v3_delete_glossary.py b/samples/snippets/translate_v3_delete_glossary.py index fb9f86af..336b7a06 100644 --- a/samples/snippets/translate_v3_delete_glossary.py +++ b/samples/snippets/translate_v3_delete_glossary.py @@ -17,9 +17,7 @@ def delete_glossary( - project_id="YOUR_PROJECT_ID", - glossary_id="YOUR_GLOSSARY_ID", - timeout=180, + project_id="YOUR_PROJECT_ID", glossary_id="YOUR_GLOSSARY_ID", timeout=180, ): """Delete a specific glossary based on the glossary ID.""" client = translate.TranslationServiceClient() diff --git a/samples/snippets/translate_v3_detect_language.py b/samples/snippets/translate_v3_detect_language.py index 9e92b607..eae985c3 100644 --- a/samples/snippets/translate_v3_detect_language.py +++ b/samples/snippets/translate_v3_detect_language.py @@ -37,9 +37,9 @@ def detect_language(project_id="YOUR_PROJECT_ID"): # The most probable language is first. for language in response.languages: # The language detected - print(u"Language code: {}".format(language.language_code)) + print("Language code: {}".format(language.language_code)) # Confidence of detection result for this language - print(u"Confidence: {}".format(language.confidence)) + print("Confidence: {}".format(language.confidence)) # [END translate_v3_detect_language] diff --git a/samples/snippets/translate_v3_get_glossary_test.py b/samples/snippets/translate_v3_get_glossary_test.py index 96ea6b78..a4fc3231 100644 --- a/samples/snippets/translate_v3_get_glossary_test.py +++ b/samples/snippets/translate_v3_get_glossary_test.py @@ -45,11 +45,11 @@ def glossary(): ) def delete_glossary(): try: - translate_v3_delete_glossary.delete_glossary( - PROJECT_ID, glossary_id) + translate_v3_delete_glossary.delete_glossary(PROJECT_ID, glossary_id) except NotFound as e: # Ignoring this case. print("Got NotFound, detail: {}".format(str(e))) + delete_glossary() diff --git a/samples/snippets/translate_v3_get_supported_languages.py b/samples/snippets/translate_v3_get_supported_languages.py index db779278..3802a7e1 100644 --- a/samples/snippets/translate_v3_get_supported_languages.py +++ b/samples/snippets/translate_v3_get_supported_languages.py @@ -29,7 +29,7 @@ def get_supported_languages(project_id="YOUR_PROJECT_ID"): # List language codes of supported languages. print("Supported Languages:") for language in response.languages: - print(u"Language Code: {}".format(language.language_code)) + print("Language Code: {}".format(language.language_code)) # [END translate_v3_get_supported_languages] diff --git a/samples/snippets/translate_v3_get_supported_languages_with_target.py b/samples/snippets/translate_v3_get_supported_languages_with_target.py index 3a164dcb..856b3178 100644 --- a/samples/snippets/translate_v3_get_supported_languages_with_target.py +++ b/samples/snippets/translate_v3_get_supported_languages_with_target.py @@ -27,12 +27,12 @@ def get_supported_languages_with_target(project_id="YOUR_PROJECT_ID"): # Supported language codes: https://cloud.google.com/translate/docs/languages response = client.get_supported_languages( - display_language_code="is", # target language code - parent=parent + display_language_code="is", parent=parent # target language code ) # List language codes of supported languages for language in response.languages: - print(u"Language Code: {}".format(language.language_code)) - print(u"Display Name: {}".format(language.display_name)) + print("Language Code: {}".format(language.language_code)) + print("Display Name: {}".format(language.display_name)) + # [END translate_v3_get_supported_languages_for_target] diff --git a/samples/snippets/translate_v3_get_supported_languages_with_target_test.py b/samples/snippets/translate_v3_get_supported_languages_with_target_test.py index 8f3f52cf..9688efee 100644 --- a/samples/snippets/translate_v3_get_supported_languages_with_target_test.py +++ b/samples/snippets/translate_v3_get_supported_languages_with_target_test.py @@ -21,9 +21,7 @@ def test_list_languages_with_target(capsys): - get_supported_langs.get_supported_languages_with_target( - PROJECT_ID - ) + get_supported_langs.get_supported_languages_with_target(PROJECT_ID) out, _ = capsys.readouterr() assert u"Language Code: sq" in out assert u"Display Name: albanska" in out diff --git a/samples/snippets/translate_v3_list_glossary_test.py b/samples/snippets/translate_v3_list_glossary_test.py index 8f4eaa1a..ed2a4754 100644 --- a/samples/snippets/translate_v3_list_glossary_test.py +++ b/samples/snippets/translate_v3_list_glossary_test.py @@ -45,11 +45,11 @@ def glossary(): ) def delete_glossary(): try: - translate_v3_delete_glossary.delete_glossary( - PROJECT_ID, glossary_id) + translate_v3_delete_glossary.delete_glossary(PROJECT_ID, glossary_id) except NotFound as e: # Ignoring this case. print("Got NotFound, detail: {}".format(str(e))) + delete_glossary() diff --git a/samples/snippets/translate_v3_translate_text.py b/samples/snippets/translate_v3_translate_text.py index 78a79b4e..cdfe819f 100644 --- a/samples/snippets/translate_v3_translate_text.py +++ b/samples/snippets/translate_v3_translate_text.py @@ -33,13 +33,13 @@ def translate_text(text="YOUR_TEXT_TO_TRANSLATE", project_id="YOUR_PROJECT_ID"): "contents": [text], "mime_type": "text/plain", # mime types: text/plain, text/html "source_language_code": "en-US", - "target_language_code": "fr" + "target_language_code": "fr", } ) # Display the translation for each input text provided for translation in response.translations: - print(u"Translated text: {}".format(translation.translated_text)) + print("Translated text: {}".format(translation.translated_text)) # [END translate_v3_translate_text] diff --git a/samples/snippets/translate_v3_translate_text_test.py b/samples/snippets/translate_v3_translate_text_test.py index c93cb91f..fee6e771 100644 --- a/samples/snippets/translate_v3_translate_text_test.py +++ b/samples/snippets/translate_v3_translate_text_test.py @@ -21,7 +21,6 @@ def test_translate_text(capsys): - translate_v3_translate_text.translate_text( - "Hello World!", PROJECT_ID) + translate_v3_translate_text.translate_text("Hello World!", PROJECT_ID) out, _ = capsys.readouterr() assert "Bonjour le monde" in out diff --git a/samples/snippets/translate_v3_translate_text_with_glossary.py b/samples/snippets/translate_v3_translate_text_with_glossary.py index b62d5413..addd737f 100644 --- a/samples/snippets/translate_v3_translate_text_with_glossary.py +++ b/samples/snippets/translate_v3_translate_text_with_glossary.py @@ -32,8 +32,7 @@ def translate_text_with_glossary( project_id, "us-central1", glossary_id # The location of the glossary ) - glossary_config = translate.TranslateTextGlossaryConfig( - glossary=glossary) + glossary_config = translate.TranslateTextGlossaryConfig(glossary=glossary) # Supported language codes: https://cloud.google.com/translate/docs/languages response = client.translate_text( @@ -42,13 +41,13 @@ def translate_text_with_glossary( "target_language_code": "ja", "source_language_code": "en", "parent": parent, - "glossary_config": glossary_config + "glossary_config": glossary_config, } ) print("Translated text: \n") for translation in response.glossary_translations: - print(u"\t {}".format(translation.translated_text)) + print("\t {}".format(translation.translated_text)) # [END translate_v3_translate_text_with_glossary] diff --git a/samples/snippets/translate_v3_translate_text_with_glossary_test.py b/samples/snippets/translate_v3_translate_text_with_glossary_test.py index 1caa9e6e..46724dde 100644 --- a/samples/snippets/translate_v3_translate_text_with_glossary_test.py +++ b/samples/snippets/translate_v3_translate_text_with_glossary_test.py @@ -46,11 +46,11 @@ def glossary(): ) def delete_glossary(): try: - translate_v3_delete_glossary.delete_glossary( - PROJECT_ID, glossary_id) + translate_v3_delete_glossary.delete_glossary(PROJECT_ID, glossary_id) except NotFound as e: # Ignoring this case. print("Got NotFound, detail: {}".format(str(e))) + delete_glossary() diff --git a/samples/snippets/translate_v3_translate_text_with_model.py b/samples/snippets/translate_v3_translate_text_with_model.py index 44c92f96..bd61bb76 100644 --- a/samples/snippets/translate_v3_translate_text_with_model.py +++ b/samples/snippets/translate_v3_translate_text_with_model.py @@ -40,11 +40,10 @@ def translate_text_with_model( "parent": parent, "mime_type": "text/plain", # mime types: text/plain, text/html } - ) # Display the translation for each input text provided for translation in response.translations: - print(u"Translated text: {}".format(translation.translated_text)) + print("Translated text: {}".format(translation.translated_text)) # [END translate_v3_translate_text_with_model] From 7e2f7fbf3743bf032633580e96e9fe86b6b987fc Mon Sep 17 00:00:00 2001 From: Bu Sun Kim Date: Tue, 1 Sep 2020 19:22:31 +0000 Subject: [PATCH 31/50] chore: delete comments --- ...te_v3_batch_translate_text_with_glossary_and_model_test.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py index a750f0df..6ba15118 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py @@ -69,7 +69,3 @@ def test_batch_translate_text_with_glossary_and_model(capsys, bucket, glossary): out, _ = capsys.readouterr() assert "Total Characters: 25" in out - # TODO: find a way to make sure it translates correctly - # SHOULD NOT BE - Google NMT model -> γγ‚Œγ―γ—γΎγ›γ‚“γ€‚ζ¬Ίception" - # literal: "γγ‚Œγ―γγ†γ " # custom model - # literal: "欺く" # glossary From df7178883022c1e49a85b02fb6841141036cd933 Mon Sep 17 00:00:00 2001 From: Bu Sun Kim Date: Tue, 1 Sep 2020 20:28:04 +0000 Subject: [PATCH 32/50] fix: fix package name --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 16f45f97..ca4e6935 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,3 @@ -oogle-cloud-translate==3.0.0 +google-cloud-translate==3.0.0 google-cloud-storage==1.30.0 google-cloud-automl==1.0.1 From c3705c421120935bbfd1ca159ed1e7a6092eae80 Mon Sep 17 00:00:00 2001 From: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Date: Wed, 2 Sep 2020 08:34:00 -0600 Subject: [PATCH 33/50] test: remove usage of location_path (#58) This resource helper was removed in the 3.0 release. Co-authored-by: Mike <45373284+munkhuushmgl@users.noreply.github.com> --- tests/system/test_vpcsc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/test_vpcsc.py b/tests/system/test_vpcsc.py index 824db401..5c560d15 100644 --- a/tests/system/test_vpcsc.py +++ b/tests/system/test_vpcsc.py @@ -31,12 +31,12 @@ def client(): @pytest.fixture(scope="module") def parent_inside(client): - return client.location_path(vpcsc_config.project_inside, "us-central1") + return f"projects/{vpcsc_config.project_inside}/locations/us-central1" @pytest.fixture(scope="module") def parent_outside(client): - return client.location_path(vpcsc_config.project_outside, "us-central1") + return f"projects/{vpcsc_config.project_outside}/locations/us-central1" @pytest.fixture(scope="module") From 1565bd2d5c0b62220d71b13f6f8d9ccb45c4fd71 Mon Sep 17 00:00:00 2001 From: Mike <45373284+munkhuushmgl@users.noreply.github.com> Date: Wed, 2 Sep 2020 09:58:04 -0700 Subject: [PATCH 34/50] chore: update version to latest (#63) --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index ca4e6935..74c2243f 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-translate==3.0.0 +google-cloud-translate==3.0.1 google-cloud-storage==1.30.0 google-cloud-automl==1.0.1 From 56a8beb95b373b49e161aebdaa3e7afce56be247 Mon Sep 17 00:00:00 2001 From: Eric Schmidt Date: Wed, 9 Sep 2020 10:26:50 -0700 Subject: [PATCH 35/50] fix: adjusts flaky test; deletes unneeded test (#66) --- .../hybrid_glossaries/hybrid_tutorial_test.py | 20 ++---------- .../resources/non_standard_format.png | Bin 138990 -> 0 bytes .../resources/non_standard_format.txt | 30 ------------------ 3 files changed, 3 insertions(+), 47 deletions(-) delete mode 100644 samples/snippets/hybrid_glossaries/resources/non_standard_format.png delete mode 100644 samples/snippets/hybrid_glossaries/resources/non_standard_format.txt diff --git a/samples/snippets/hybrid_glossaries/hybrid_tutorial_test.py b/samples/snippets/hybrid_glossaries/hybrid_tutorial_test.py index eaccf7f2..2b4f6559 100644 --- a/samples/snippets/hybrid_glossaries/hybrid_tutorial_test.py +++ b/samples/snippets/hybrid_glossaries/hybrid_tutorial_test.py @@ -28,25 +28,11 @@ # VISION TESTS -def test_vision_standard_format(capsys): +def test_vision_standard_format(): # Generate text using Vision API - pic_to_text('resources/standard_format.jpeg') - out, err = capsys.readouterr() - - assert 'Detected text:' in out - assert 'test!' in out - - -def test_vision_non_standard_format(): + text = pic_to_text('resources/standard_format.jpeg') - # Generate text - text = pic_to_text("resources/non_standard_format.png") - - # Read expected text - with open("resources/non_standard_format.txt") as f: - expected_text = f.read() - - assert text == expected_text + assert len(text) > 0 # TRANSLATE TESTS diff --git a/samples/snippets/hybrid_glossaries/resources/non_standard_format.png b/samples/snippets/hybrid_glossaries/resources/non_standard_format.png deleted file mode 100644 index eeee9c7f6a6fe57b3d5dc84a63b6ae9e04e37e52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138990 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�dw@jIA}60uH}U512OV|r3bAc^ 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

  • 2_^)PyNj~VsxIk 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

    Iy%1l023^7IvM_Ot>326JD4 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 diff --git a/samples/snippets/hybrid_glossaries/resources/non_standard_format.txt b/samples/snippets/hybrid_glossaries/resources/non_standard_format.txt deleted file mode 100644 index 529799ee..00000000 --- a/samples/snippets/hybrid_glossaries/resources/non_standard_format.txt +++ /dev/null @@ -1,30 +0,0 @@ -MENU -Google Cloud Bistro -SALADS -SANDWICHES -GCP Green Salad -Fresh Greens -$5 -Kubernetes Sandwich -ham and cheese sandwich -$10 -Cloud Caprese -Mozzarella, tomatoes, basil, -balsamic reduction -$8 -Dialogflow Panini -chicken, pesto, and -mozzarella panini -$10 -Firebase Fruit Salad -watermelon, honeydew melon, -and pineapple -$6 -Compute Engine Burger -quarter-pound burger with -cheddar cheese -$10 -BigQuery BLT -bacon, lettuce, and tomato -sandwich -$10 From f5dc3a02a15cc92618594310ab93215e82f976e3 Mon Sep 17 00:00:00 2001 From: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Date: Thu, 10 Sep 2020 14:29:35 -0600 Subject: [PATCH 36/50] test: fix broken vpcsc tests (#65) * test_vpcsc.test_batch_translate_text_w_outside * test_vpcsc.test_list_glossaries_w_inside --- tests/system/test_vpcsc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/test_vpcsc.py b/tests/system/test_vpcsc.py index 5c560d15..e3bf098b 100644 --- a/tests/system/test_vpcsc.py +++ b/tests/system/test_vpcsc.py @@ -88,7 +88,7 @@ def test_create_glossary_w_outside(client, parent_outside, glossary_outside): @vpcsc_config.skip_unless_inside_vpcsc def test_list_glossaries_w_inside(client, parent_inside): - list(client.list_glossaries(parent_inside)) + list(client.list_glossaries(parent=parent_inside)) @vpcsc_config.skip_unless_inside_vpcsc @@ -161,7 +161,7 @@ def test_batch_translate_text_w_outside(client, parent_outside): with pytest.raises(exceptions.PermissionDenied) as exc: client.batch_translate_text( request={ - "parent": parent_inside, + "parent": parent_outside, "source_language_code": source_language_code, "target_language_codes": target_language_codes, "input_configs": input_configs, From eebeb5b9632de294233b19e39eaaba6a6d88b1d8 Mon Sep 17 00:00:00 2001 From: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Date: Mon, 28 Sep 2020 13:16:37 -0600 Subject: [PATCH 37/50] chore: add default CODEOWNERS (#70) --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 65116f60..f8d4cbae 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,6 +4,9 @@ # For syntax help see: # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax +# The @googleapis/yoshi-python is the default owner for changes in this repo +* @googleapis/yoshi-python + # The python-samples-owners team is the default owner for samples /samples/**/*.py @telpirion @sirtorry @googleapis/python-samples-owners \ No newline at end of file From 2f96f2ec8e31fa345cabd790b609d60c0a696e3a Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Tue, 6 Oct 2020 16:41:48 -0500 Subject: [PATCH 38/50] test: update tests to support latest google-cloud-core (#69) `google-cloud-core` version 1.4.2 populates `prettyPrint=false` by default. Update the connection tests to expect a value for `prettyPrint`. --- tests/unit/v2/test__http.py | 57 ++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/tests/unit/v2/test__http.py b/tests/unit/v2/test__http.py index 4830c0c8..c48cc4bb 100644 --- a/tests/unit/v2/test__http.py +++ b/tests/unit/v2/test__http.py @@ -28,30 +28,56 @@ def _make_one(self, *args, **kw): return self._get_target_class()(*args, **kw) def test_build_api_url_no_extra_query_params(self): + from six.moves.urllib.parse import parse_qsl + from six.moves.urllib.parse import urlsplit + conn = self._make_one(object()) - URI = "/".join( - [ - conn.DEFAULT_API_ENDPOINT, - "language", - "translate", - conn.API_VERSION, - "foo", - ] + uri = conn.build_api_url("/foo") + scheme, netloc, path, qs, _ = urlsplit(uri) + self.assertEqual("%s://%s" % (scheme, netloc), conn.API_BASE_URL) + self.assertEqual( + path, "/".join(["", "language", "translate", conn.API_VERSION, "foo"]) ) - self.assertEqual(conn.build_api_url("/foo"), URI) + parms = dict(parse_qsl(qs)) + pretty_print = parms.pop("prettyPrint", "false") + self.assertEqual(pretty_print, "false") + self.assertEqual(parms, {}) def test_build_api_url_w_custom_endpoint(self): + from six.moves.urllib.parse import parse_qsl + from six.moves.urllib.parse import urlsplit + custom_endpoint = "https://foo-translation.googleapis.com" conn = self._make_one(object(), api_endpoint=custom_endpoint) - URI = "/".join( - [custom_endpoint, "language", "translate", conn.API_VERSION, "foo"] + uri = conn.build_api_url("/foo") + scheme, netloc, path, qs, _ = urlsplit(uri) + self.assertEqual("%s://%s" % (scheme, netloc), custom_endpoint) + self.assertEqual( + path, "/".join(["", "language", "translate", conn.API_VERSION, "foo"]) ) - self.assertEqual(conn.build_api_url("/foo"), URI) + parms = dict(parse_qsl(qs)) + pretty_print = parms.pop("prettyPrint", "false") + self.assertEqual(pretty_print, "false") + self.assertEqual(parms, {}) def test_build_api_url_w_extra_query_params(self): from six.moves.urllib.parse import parse_qsl from six.moves.urllib.parse import urlsplit + conn = self._make_one(object()) + uri = conn.build_api_url("/foo", {"bar": "baz"}) + scheme, netloc, path, qs, _ = urlsplit(uri) + self.assertEqual("%s://%s" % (scheme, netloc), conn.API_BASE_URL) + self.assertEqual( + path, "/".join(["", "language", "translate", conn.API_VERSION, "foo"]) + ) + parms = dict(parse_qsl(qs)) + self.assertEqual(parms["bar"], "baz") + + def test_build_api_url_w_extra_query_params_tuple(self): + from six.moves.urllib.parse import parse_qsl + from six.moves.urllib.parse import urlsplit + conn = self._make_one(object()) query_params = [("q", "val1"), ("q", "val2")] uri = conn.build_api_url("/foo", query_params=query_params) @@ -59,8 +85,11 @@ def test_build_api_url_w_extra_query_params(self): self.assertEqual("%s://%s" % (scheme, netloc), conn.API_BASE_URL) expected_path = "/".join(["", "language", "translate", conn.API_VERSION, "foo"]) self.assertEqual(path, expected_path) - params = parse_qsl(qs) - self.assertEqual(params, query_params) + params = list( + sorted(param for param in parse_qsl(qs) if param[0] != "prettyPrint") + ) + expected_params = [("q", "val1"), ("q", "val2")] + self.assertEqual(params, expected_params) def test_extra_headers(self): import requests From 4f60ef8bca920b48548f0068c88111981f7a33ce Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 13 Oct 2020 22:36:33 +0200 Subject: [PATCH 39/50] chore(deps): update dependency google-cloud-vision to v2 (#71) * chore(deps): update dependency google-cloud-vision to v2 * remove types Co-authored-by: Leah Cole Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- .../snippets/hybrid_glossaries/hybrid_tutorial.py | 12 ++++++------ samples/snippets/hybrid_glossaries/requirements.txt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/samples/snippets/hybrid_glossaries/hybrid_tutorial.py b/samples/snippets/hybrid_glossaries/hybrid_tutorial.py index c98bdd0e..9eae168a 100644 --- a/samples/snippets/hybrid_glossaries/hybrid_tutorial.py +++ b/samples/snippets/hybrid_glossaries/hybrid_tutorial.py @@ -51,7 +51,7 @@ def pic_to_text(infile): with io.open(infile, "rb") as image_file: content = image_file.read() - image = vision.types.Image(content=content) + image = vision.Image(content=content) # For dense text, use document_text_detection # For less dense text, use text_detection @@ -88,16 +88,16 @@ def create_glossary(languages, project_id, glossary_name, glossary_uri): name = client.glossary_path(project_id, location, glossary_name) # Set language codes - language_codes_set = translate.types.Glossary.LanguageCodesSet( + language_codes_set = translate.Glossary.LanguageCodesSet( language_codes=languages ) - gcs_source = translate.types.GcsSource(input_uri=glossary_uri) + gcs_source = translate.GcsSource(input_uri=glossary_uri) - input_config = translate.types.GlossaryInputConfig(gcs_source=gcs_source) + input_config = translate.GlossaryInputConfig(gcs_source=gcs_source) # Set glossary resource information - glossary = translate.types.Glossary( + glossary = translate.Glossary( name=name, language_codes_set=language_codes_set, input_config=input_config ) @@ -145,7 +145,7 @@ def translate_text( glossary = client.glossary_path(project_id, location, glossary_name) - glossary_config = translate.types.TranslateTextGlossaryConfig(glossary=glossary) + glossary_config = translate.TranslateTextGlossaryConfig(glossary=glossary) parent = f"projects/{project_id}/locations/{location}" diff --git a/samples/snippets/hybrid_glossaries/requirements.txt b/samples/snippets/hybrid_glossaries/requirements.txt index af2fa067..86b3d1b3 100644 --- a/samples/snippets/hybrid_glossaries/requirements.txt +++ b/samples/snippets/hybrid_glossaries/requirements.txt @@ -1,3 +1,3 @@ google-cloud-translate==3.0.0 -google-cloud-vision==1.0.0 +google-cloud-vision==2.0.0 google-cloud-texttospeech==2.1.0 From 18ce10894450c80c759cd1dd0d8fafa45ca1d63a Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 16 Oct 2020 21:30:47 +0200 Subject: [PATCH 40/50] chore(deps): update dependency google-cloud-texttospeech to v2.2.0 (#49) --- samples/snippets/hybrid_glossaries/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/hybrid_glossaries/requirements.txt b/samples/snippets/hybrid_glossaries/requirements.txt index 86b3d1b3..24dfa306 100644 --- a/samples/snippets/hybrid_glossaries/requirements.txt +++ b/samples/snippets/hybrid_glossaries/requirements.txt @@ -1,3 +1,3 @@ google-cloud-translate==3.0.0 google-cloud-vision==2.0.0 -google-cloud-texttospeech==2.1.0 +google-cloud-texttospeech==2.2.0 From 84b56efd3f3e7550598a09adcb38c5967dfc74f8 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 16 Oct 2020 21:40:05 +0200 Subject: [PATCH 41/50] chore(deps): update dependency google-cloud-storage to v1.31.2 (#55) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [google-cloud-storage](https://togithub.com/googleapis/python-storage) | minor | `==1.30.0` -> `==1.31.2` | --- ### Release Notes

    googleapis/python-storage ### [`v1.31.2`](https://togithub.com/googleapis/python-storage/blob/master/CHANGELOG.md#​1312-httpswwwgithubcomgoogleapispython-storagecomparev1311v1312-2020-09-23) [Compare Source](https://togithub.com/googleapis/python-storage/compare/v1.31.1...v1.31.2) ### [`v1.31.1`](https://togithub.com/googleapis/python-storage/blob/master/CHANGELOG.md#​1311-httpswwwgithubcomgoogleapispython-storagecomparev1310v1311-2020-09-16) [Compare Source](https://togithub.com/googleapis/python-storage/compare/v1.31.0...v1.31.1) ### [`v1.31.0`](https://togithub.com/googleapis/python-storage/blob/master/CHANGELOG.md#​1310-httpswwwgithubcomgoogleapispython-storagecomparev1300v1310-2020-08-26) [Compare Source](https://togithub.com/googleapis/python-storage/compare/v1.30.0...v1.31.0) ##### Features - add configurable checksumming for blob uploads and downloads ([#​246](https://www.github.com/googleapis/python-storage/issues/246)) ([23b7d1c](https://www.github.com/googleapis/python-storage/commit/23b7d1c3155deae3c804c510dee3a7cec97cd46c)) - add support for 'Blob.custom_time' and lifecycle rules ([#​199](https://www.github.com/googleapis/python-storage/issues/199)) ([180873d](https://www.github.com/googleapis/python-storage/commit/180873de139f7f8e00b7bef423bc15760cf68cc2)) - error message return from api ([#​235](https://www.github.com/googleapis/python-storage/issues/235)) ([a8de586](https://www.github.com/googleapis/python-storage/commit/a8de5868f32b45868f178f420138fcd2fe42f5fd)) - **storage:** add support of daysSinceNoncurrentTime and noncurrentTimeBefore ([#​162](https://www.github.com/googleapis/python-storage/issues/162)) ([136c097](https://www.github.com/googleapis/python-storage/commit/136c0970f8ef7ad4751104e3b8b7dd3204220a67)) - pass 'client_options' to base class ctor ([#​225](https://www.github.com/googleapis/python-storage/issues/225)) ([e1f91fc](https://www.github.com/googleapis/python-storage/commit/e1f91fcca6c001bc3b0c5f759a7a003fcf60c0a6)), closes [#​210](https://www.github.com/googleapis/python-storage/issues/210) - rename 'Blob.download_as_{string,bytes}', add 'Blob.download_as_text' ([#​182](https://www.github.com/googleapis/python-storage/issues/182)) ([73107c3](https://www.github.com/googleapis/python-storage/commit/73107c35f23c4a358e957c2b8188300a7fa958fe)) ##### Bug Fixes - change datetime.now to utcnow ([#​251](https://www.github.com/googleapis/python-storage/issues/251)) ([3465d08](https://www.github.com/googleapis/python-storage/commit/3465d08e098edb250dee5e97d1fb9ded8bae5700)), closes [#​228](https://www.github.com/googleapis/python-storage/issues/228) - extract hashes correctly during download ([#​238](https://www.github.com/googleapis/python-storage/issues/238)) ([23cfb65](https://www.github.com/googleapis/python-storage/commit/23cfb65c3a3b10759c67846e162e4ed77a3f5307)) - repair mal-formed docstring ([#​255](https://www.github.com/googleapis/python-storage/issues/255)) ([e722376](https://www.github.com/googleapis/python-storage/commit/e722376371cb8a3acc46d6c84fb41f4e874f41aa)) ##### Documentation - update docs build (via synth) ([#​222](https://www.github.com/googleapis/python-storage/issues/222)) ([4c5adfa](https://www.github.com/googleapis/python-storage/commit/4c5adfa6e05bf018d72ee1a7e99679fd55f2c662))
    --- ### Renovate configuration :date: **Schedule**: At any time (no schedule defined). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/python-translate). --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 74c2243f..3c4ef316 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,3 @@ google-cloud-translate==3.0.1 -google-cloud-storage==1.30.0 +google-cloud-storage==1.31.2 google-cloud-automl==1.0.1 From 707bc88b82b7af71f543f6cfafb2a0badf920d77 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 16 Oct 2020 22:00:07 +0200 Subject: [PATCH 42/50] chore(deps): update dependency google-cloud-translate to v3.0.1 (#45) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [google-cloud-translate](https://togithub.com/googleapis/python-translate) | patch | `==3.0.0` -> `==3.0.1` | --- ### Release Notes
    googleapis/python-translate ### [`v3.0.1`](https://togithub.com/googleapis/python-translate/blob/master/CHANGELOG.md#​301-httpswwwgithubcomgoogleapispython-translatecomparev300v301-2020-08-08) [Compare Source](https://togithub.com/googleapis/python-translate/compare/v3.0.0...v3.0.1)
    --- ### Renovate configuration :date: **Schedule**: At any time (no schedule defined). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/python-translate). --- samples/snippets/hybrid_glossaries/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/hybrid_glossaries/requirements.txt b/samples/snippets/hybrid_glossaries/requirements.txt index 24dfa306..ff0077c1 100644 --- a/samples/snippets/hybrid_glossaries/requirements.txt +++ b/samples/snippets/hybrid_glossaries/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-translate==3.0.0 +google-cloud-translate==3.0.1 google-cloud-vision==2.0.0 google-cloud-texttospeech==2.2.0 From 66b577b1f8a648dc8aaae5c04b2d0b37b98757f3 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 16 Oct 2020 22:16:06 +0200 Subject: [PATCH 43/50] chore(deps): update dependency google-cloud-automl to v2 (#67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | [google-cloud-automl](https://togithub.com/googleapis/python-automl) | major | `==1.0.1` -> `==2.0.0` | --- ### Release Notes
    googleapis/python-automl ### [`v2.0.0`](https://togithub.com/googleapis/python-automl/blob/master/CHANGELOG.md#​200-httpswwwgithubcomgoogleapispython-automlcomparev101v200-2020-09-16) [Compare Source](https://togithub.com/googleapis/python-automl/compare/v1.0.1...v2.0.0) ##### ⚠ BREAKING CHANGES - move to microgen. See [Migration Guide](https://togithub.com/googleapis/python-automl/blob/release-v2.0.0/UPGRADING.md) ([#​61](https://togithub.com/googleapis/python-automl/issues/61)) ##### Features - move to microgen ([#​61](https://www.github.com/googleapis/python-automl/issues/61)) ([009085e](https://www.github.com/googleapis/python-automl/commit/009085e0a82d1d7729349746c2c8954d5d60e0a9)) ##### Bug Fixes - **translate:** fix a broken test [(\[#​4360\](https://www.github.com/googleapis/python-automl/issues/4360))](https://togithub.com/GoogleCloudPlatform/python-docs-samples/issues/4360) ([5f7d141](https://www.github.com/googleapis/python-automl/commit/5f7d141afe732acf7458a9ac98618e93baa93d38)), closes [#​4353](https://www.github.com/googleapis/python-automl/issues/4353) - `update_column_spec` typo in TablesClient docstring ([#​18](https://www.github.com/googleapis/python-automl/issues/18)) ([9feb4cc](https://www.github.com/googleapis/python-automl/commit/9feb4cc5e04a01a4199da43400457cca6c0bfa05)), closes [#​17](https://www.github.com/googleapis/python-automl/issues/17) - update retry configs ([#​44](https://www.github.com/googleapis/python-automl/issues/44)) ([7df9059](https://www.github.com/googleapis/python-automl/commit/7df905910b86721a6ee3a3b6c916a4f8e27d0aa7)) ##### Documentation - add cancel operation sample ([abc5070](https://www.github.com/googleapis/python-automl/commit/abc507005d5255ed5adf2c4b8e0b23042a0bdf47)) - add samples from tables/automl ([#​54](https://www.github.com/googleapis/python-automl/issues/54)) ([d225a5f](https://www.github.com/googleapis/python-automl/commit/d225a5f97c2823218b91a79e77d3383132875231)), closes [#​2090](https://www.github.com/googleapis/python-automl/issues/2090) [#​2100](https://www.github.com/googleapis/python-automl/issues/2100) [#​2102](https://www.github.com/googleapis/python-automl/issues/2102) [#​2103](https://www.github.com/googleapis/python-automl/issues/2103) [#​2101](https://www.github.com/googleapis/python-automl/issues/2101) [#​2110](https://www.github.com/googleapis/python-automl/issues/2110) [#​2115](https://www.github.com/googleapis/python-automl/issues/2115) [#​2150](https://www.github.com/googleapis/python-automl/issues/2150) [#​2145](https://www.github.com/googleapis/python-automl/issues/2145) [#​2203](https://www.github.com/googleapis/python-automl/issues/2203) [#​2340](https://www.github.com/googleapis/python-automl/issues/2340) [#​2337](https://www.github.com/googleapis/python-automl/issues/2337) [#​2336](https://www.github.com/googleapis/python-automl/issues/2336) [#​2339](https://www.github.com/googleapis/python-automl/issues/2339) [#​2338](https://www.github.com/googleapis/python-automl/issues/2338) [#​2276](https://www.github.com/googleapis/python-automl/issues/2276) [#​2257](https://www.github.com/googleapis/python-automl/issues/2257) [#​2424](https://www.github.com/googleapis/python-automl/issues/2424) [#​2407](https://www.github.com/googleapis/python-automl/issues/2407) [#​2501](https://www.github.com/googleapis/python-automl/issues/2501) [#​2459](https://www.github.com/googleapis/python-automl/issues/2459) [#​2601](https://www.github.com/googleapis/python-automl/issues/2601) [#​2523](https://www.github.com/googleapis/python-automl/issues/2523) [#​2005](https://www.github.com/googleapis/python-automl/issues/2005) [#​3033](https://www.github.com/googleapis/python-automl/issues/3033) [#​2806](https://www.github.com/googleapis/python-automl/issues/2806) [#​3750](https://www.github.com/googleapis/python-automl/issues/3750) [#​3571](https://www.github.com/googleapis/python-automl/issues/3571) [#​3929](https://www.github.com/googleapis/python-automl/issues/3929) [#​4022](https://www.github.com/googleapis/python-automl/issues/4022) [#​4127](https://www.github.com/googleapis/python-automl/issues/4127) ##### [1.0.1](https://www.github.com/googleapis/python-automl/compare/v1.0.0...v1.0.1) (2020-06-18) ##### Bug Fixes - fixes release status trove classifier ([#​39](https://www.github.com/googleapis/python-automl/issues/39)) ([5b5d6c3](https://www.github.com/googleapis/python-automl/commit/5b5d6c33178f4f052cba01cc08cf3023d4303d7a))
    --- ### Renovate configuration :date: **Schedule**: At any time (no schedule defined). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/python-translate). --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 3c4ef316..aa75e4fb 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,3 @@ google-cloud-translate==3.0.1 google-cloud-storage==1.31.2 -google-cloud-automl==1.0.1 +google-cloud-automl==2.0.0 From 2bc6296122e54bc93804d37411ff7554e2808626 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 23 Oct 2020 20:49:36 +0200 Subject: [PATCH 44/50] chore(deps): update dependency google-cloud-storage to v1.32.0 (#72) --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index aa75e4fb..5896f2e2 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,3 @@ google-cloud-translate==3.0.1 -google-cloud-storage==1.31.2 +google-cloud-storage==1.32.0 google-cloud-automl==2.0.0 From 37e382605e37640f936ed49778f54e00be332d6d Mon Sep 17 00:00:00 2001 From: arithmetic1728 <58957152+arithmetic1728@users.noreply.github.com> Date: Mon, 26 Oct 2020 16:59:24 -0700 Subject: [PATCH 45/50] fix: re-generated v3 client and fix system.py (#76) * fix: re-generated v3 client * fix: skip v2 client system test for mtls testing --- .github/snippet-bot.yml | 0 .kokoro/docs/common.cfg | 2 +- .kokoro/populate-secrets.sh | 43 ++ .kokoro/release/common.cfg | 50 +- .kokoro/samples/python3.6/common.cfg | 6 + .kokoro/samples/python3.7/common.cfg | 6 + .kokoro/samples/python3.8/common.cfg | 6 + .kokoro/test-samples.sh | 8 +- .kokoro/trampoline.sh | 15 +- CODE_OF_CONDUCT.md | 123 ++-- docs/conf.py | 3 +- .../translation_service/async_client.py | 52 +- .../services/translation_service/client.py | 105 ++-- .../translation_service/transports/base.py | 32 +- .../translation_service/transports/grpc.py | 62 +- .../transports/grpc_asyncio.py | 61 +- .../translation_service/async_client.py | 60 +- .../services/translation_service/client.py | 105 ++-- .../translation_service/transports/base.py | 40 +- .../translation_service/transports/grpc.py | 62 +- .../transports/grpc_asyncio.py | 61 +- noxfile.py | 4 +- samples/snippets/hybrid_glossaries/noxfile.py | 5 + samples/snippets/noxfile.py | 11 +- scripts/decrypt-secrets.sh | 15 +- synth.metadata | 8 +- tests/system.py | 12 + .../translate_v3/test_translation_service.py | 551 ++++++++++-------- .../test_translation_service.py | 551 ++++++++++-------- 29 files changed, 1267 insertions(+), 792 deletions(-) create mode 100644 .github/snippet-bot.yml create mode 100755 .kokoro/populate-secrets.sh diff --git a/.github/snippet-bot.yml b/.github/snippet-bot.yml new file mode 100644 index 00000000..e69de29b diff --git a/.kokoro/docs/common.cfg b/.kokoro/docs/common.cfg index 95963d11..180702da 100644 --- a/.kokoro/docs/common.cfg +++ b/.kokoro/docs/common.cfg @@ -30,7 +30,7 @@ env_vars: { env_vars: { key: "V2_STAGING_BUCKET" - value: "docs-staging-v2-staging" + value: "docs-staging-v2" } # It will upload the docker image after successful builds. diff --git a/.kokoro/populate-secrets.sh b/.kokoro/populate-secrets.sh new file mode 100755 index 00000000..f5251425 --- /dev/null +++ b/.kokoro/populate-secrets.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# Copyright 2020 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 +# +# http://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. + +set -eo pipefail + +function now { date +"%Y-%m-%d %H:%M:%S" | tr -d '\n' ;} +function msg { println "$*" >&2 ;} +function println { printf '%s\n' "$(now) $*" ;} + + +# Populates requested secrets set in SECRET_MANAGER_KEYS from service account: +# kokoro-trampoline@cloud-devrel-kokoro-resources.iam.gserviceaccount.com +SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager" +msg "Creating folder on disk for secrets: ${SECRET_LOCATION}" +mkdir -p ${SECRET_LOCATION} +for key in $(echo ${SECRET_MANAGER_KEYS} | sed "s/,/ /g") +do + msg "Retrieving secret ${key}" + docker run --entrypoint=gcloud \ + --volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR} \ + gcr.io/google.com/cloudsdktool/cloud-sdk \ + secrets versions access latest \ + --project cloud-devrel-kokoro-resources \ + --secret ${key} > \ + "${SECRET_LOCATION}/${key}" + if [[ $? == 0 ]]; then + msg "Secret written to ${SECRET_LOCATION}/${key}" + else + msg "Error retrieving secret ${key}" + fi +done diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg index ef255b9f..14180c27 100644 --- a/.kokoro/release/common.cfg +++ b/.kokoro/release/common.cfg @@ -23,42 +23,18 @@ env_vars: { value: "github/python-translate/.kokoro/release.sh" } -# Fetch the token needed for reporting release status to GitHub -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "yoshi-automation-github-key" - } - } -} - -# Fetch PyPI password -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "google_cloud_pypi_password" - } - } -} - -# Fetch magictoken to use with Magic Github Proxy -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "releasetool-magictoken" - } - } +# Fetch PyPI password +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "google_cloud_pypi_password" + } + } } -# Fetch api key to use with Magic Github Proxy -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "magic-github-proxy-api-key" - } - } -} +# Tokens needed to report release status back to GitHub +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.6/common.cfg b/.kokoro/samples/python3.6/common.cfg index 0d5ec9ab..0afe4cf9 100644 --- a/.kokoro/samples/python3.6/common.cfg +++ b/.kokoro/samples/python3.6/common.cfg @@ -13,6 +13,12 @@ env_vars: { value: "py-3.6" } +# Declare build specific Cloud project. +env_vars: { + key: "BUILD_SPECIFIC_GCLOUD_PROJECT" + value: "python-docs-samples-tests-py36" +} + env_vars: { key: "TRAMPOLINE_BUILD_FILE" value: "github/python-translate/.kokoro/test-samples.sh" diff --git a/.kokoro/samples/python3.7/common.cfg b/.kokoro/samples/python3.7/common.cfg index 59a17837..b82e68ae 100644 --- a/.kokoro/samples/python3.7/common.cfg +++ b/.kokoro/samples/python3.7/common.cfg @@ -13,6 +13,12 @@ env_vars: { value: "py-3.7" } +# Declare build specific Cloud project. +env_vars: { + key: "BUILD_SPECIFIC_GCLOUD_PROJECT" + value: "python-docs-samples-tests-py37" +} + env_vars: { key: "TRAMPOLINE_BUILD_FILE" value: "github/python-translate/.kokoro/test-samples.sh" diff --git a/.kokoro/samples/python3.8/common.cfg b/.kokoro/samples/python3.8/common.cfg index 6cff86d8..9a34eac6 100644 --- a/.kokoro/samples/python3.8/common.cfg +++ b/.kokoro/samples/python3.8/common.cfg @@ -13,6 +13,12 @@ env_vars: { value: "py-3.8" } +# Declare build specific Cloud project. +env_vars: { + key: "BUILD_SPECIFIC_GCLOUD_PROJECT" + value: "python-docs-samples-tests-py38" +} + env_vars: { key: "TRAMPOLINE_BUILD_FILE" value: "github/python-translate/.kokoro/test-samples.sh" diff --git a/.kokoro/test-samples.sh b/.kokoro/test-samples.sh index e121af3e..6409c76b 100755 --- a/.kokoro/test-samples.sh +++ b/.kokoro/test-samples.sh @@ -28,6 +28,12 @@ if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"periodic"* ]]; then git checkout $LATEST_RELEASE fi +# Exit early if samples directory doesn't exist +if [ ! -d "./samples" ]; then + echo "No tests run. `./samples` not found" + exit 0 +fi + # Disable buffering, so that the logs stream through. export PYTHONUNBUFFERED=1 @@ -101,4 +107,4 @@ cd "$ROOT" # Workaround for Kokoro permissions issue: delete secrets rm testing/{test-env.sh,client-secrets.json,service-account.json} -exit "$RTN" \ No newline at end of file +exit "$RTN" diff --git a/.kokoro/trampoline.sh b/.kokoro/trampoline.sh index e8c4251f..f39236e9 100755 --- a/.kokoro/trampoline.sh +++ b/.kokoro/trampoline.sh @@ -15,9 +15,14 @@ set -eo pipefail -python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" || ret_code=$? +# Always run the cleanup script, regardless of the success of bouncing into +# the container. +function cleanup() { + chmod +x ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh + ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh + echo "cleanup"; +} +trap cleanup EXIT -chmod +x ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh -${KOKORO_GFILE_DIR}/trampoline_cleanup.sh || true - -exit ${ret_code} +$(dirname $0)/populate-secrets.sh # Secret Manager secrets. +python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b3d1f602..039f4368 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,44 +1,95 @@ -# Contributor Code of Conduct +# Code of Conduct -As contributors and maintainers of this project, -and in the interest of fostering an open and welcoming community, -we pledge to respect all people who contribute through reporting issues, -posting feature requests, updating documentation, -submitting pull requests or patches, and other activities. +## Our Pledge -We are committed to making participation in this project -a harassment-free experience for everyone, -regardless of level of experience, gender, gender identity and expression, -sexual orientation, disability, personal appearance, -body size, race, ethnicity, age, religion, or nationality. +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery -* Personal attacks -* Trolling or insulting/derogatory comments -* Public or private harassment -* Publishing other's private information, -such as physical or electronic -addresses, without explicit permission -* Other unethical or unprofessional conduct. +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct. -By adopting this Code of Conduct, -project maintainers commit themselves to fairly and consistently -applying these principles to every aspect of managing this project. -Project maintainers who do not follow or enforce the Code of Conduct -may be permanently removed from the project team. - -This code of conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior -may be reported by opening an issue -or contacting one or more of the project maintainers. - -This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, -available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +This Code of Conduct also applies outside the project spaces when the Project +Steward has a reasonable belief that an individual's behavior may have a +negative impact on the project or its community. + +## Conflict Resolution + +We do not believe that all conflict is bad; healthy debate and disagreement +often yield positive results. However, it is never okay to be disrespectful or +to engage in behavior that violates the project’s code of conduct. + +If you see someone violating the code of conduct, you are encouraged to address +the behavior directly with those involved. Many issues can be resolved quickly +and easily, and this gives people more control over the outcome of their +dispute. If you are unable to resolve the matter for any reason, or if the +behavior is threatening or harassing, report it. We are dedicated to providing +an environment where participants feel welcome and safe. + + +Reports should be directed to *googleapis-stewards@google.com*, the +Project Steward(s) for *Google Cloud Client Libraries*. It is the Project Steward’s duty to +receive and address reported violations of the code of conduct. They will then +work with a committee consisting of representatives from the Open Source +Programs Office and the Google Open Source Strategy team. If for any reason you +are uncomfortable reaching out to the Project Steward, please email +opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. +We will use our discretion in determining when and how to follow up on reported +incidents, which may range from not taking action to permanent expulsion from +the project and project-sponsored spaces. We will notify the accused of the +report and provide them an opportunity to discuss it before any action is taken. +The identity of the reporter will be omitted from the details of the report +supplied to the accused. In potentially harmful situations, such as ongoing +harassment or threats to anyone's safety, we may take action without notice. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, +available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 57ed8f60..51457137 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,7 +29,7 @@ # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "1.6.3" +needs_sphinx = "1.5.5" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -39,6 +39,7 @@ "sphinx.ext.autosummary", "sphinx.ext.intersphinx", "sphinx.ext.coverage", + "sphinx.ext.doctest", "sphinx.ext.napoleon", "sphinx.ext.todo", "sphinx.ext.viewcode", diff --git a/google/cloud/translate_v3/services/translation_service/async_client.py b/google/cloud/translate_v3/services/translation_service/async_client.py index 9cb9b9d8..8d2c52b9 100644 --- a/google/cloud/translate_v3/services/translation_service/async_client.py +++ b/google/cloud/translate_v3/services/translation_service/async_client.py @@ -28,13 +28,13 @@ from google.auth import credentials # type: ignore from google.oauth2 import service_account # type: ignore -from google.api_core import operation -from google.api_core import operation_async +from google.api_core import operation # type: ignore +from google.api_core import operation_async # type: ignore from google.cloud.translate_v3.services.translation_service import pagers from google.cloud.translate_v3.types import translation_service from google.protobuf import timestamp_pb2 as timestamp # type: ignore -from .transports.base import TranslationServiceTransport +from .transports.base import TranslationServiceTransport, DEFAULT_CLIENT_INFO from .transports.grpc_asyncio import TranslationServiceGrpcAsyncIOTransport from .client import TranslationServiceClient @@ -48,6 +48,7 @@ class TranslationServiceAsyncClient: DEFAULT_MTLS_ENDPOINT = TranslationServiceClient.DEFAULT_MTLS_ENDPOINT glossary_path = staticmethod(TranslationServiceClient.glossary_path) + parse_glossary_path = staticmethod(TranslationServiceClient.parse_glossary_path) from_service_account_file = TranslationServiceClient.from_service_account_file from_service_account_json = from_service_account_file @@ -63,6 +64,7 @@ def __init__( credentials: credentials.Credentials = None, transport: Union[str, TranslationServiceTransport] = "grpc_asyncio", client_options: ClientOptions = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: """Instantiate the translation service client. @@ -78,16 +80,19 @@ def __init__( client_options (ClientOptions): Custom options for the client. It won't take effect if a ``transport`` instance is provided. (1) The ``api_endpoint`` property can be used to override the - default endpoint provided by the client. GOOGLE_API_USE_MTLS + default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT environment variable can also be used to override the endpoint: "always" (always use the default mTLS endpoint), "never" (always - use the default regular endpoint, this is the default value for - the environment variable) and "auto" (auto switch to the default - mTLS endpoint if client SSL credentials is present). However, - the ``api_endpoint`` property takes precedence if provided. - (2) The ``client_cert_source`` property is used to provide client - SSL credentials for mutual TLS transport. If not provided, the - default SSL credentials will be used if present. + use the default regular endpoint) and "auto" (auto switch to the + default mTLS endpoint if client certificate is present, this is + the default value). However, the ``api_endpoint`` property takes + precedence if provided. + (2) If GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide client certificate for mutual TLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -95,7 +100,10 @@ def __init__( """ self._client = TranslationServiceClient( - credentials=credentials, transport=transport, client_options=client_options, + credentials=credentials, + transport=transport, + client_options=client_options, + client_info=client_info, ) async def translate_text( @@ -248,7 +256,7 @@ async def translate_text( rpc = gapic_v1.method_async.wrap_method( self._client._transport.translate_text, default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -368,7 +376,7 @@ async def detect_language( rpc = gapic_v1.method_async.wrap_method( self._client._transport.detect_language, default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -494,7 +502,7 @@ async def get_supported_languages( ), ), default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -558,7 +566,7 @@ async def batch_translate_text( rpc = gapic_v1.method_async.wrap_method( self._client._transport.batch_translate_text, default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -647,7 +655,7 @@ async def create_glossary( rpc = gapic_v1.method_async.wrap_method( self._client._transport.create_glossary, default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -737,7 +745,7 @@ async def list_glossaries( ), ), default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -822,7 +830,7 @@ async def get_glossary( ), ), default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -907,7 +915,7 @@ async def delete_glossary( ), ), default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -932,11 +940,11 @@ async def delete_glossary( try: - _client_info = gapic_v1.client_info.ClientInfo( + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=pkg_resources.get_distribution("google-cloud-translate",).version, ) except pkg_resources.DistributionNotFound: - _client_info = gapic_v1.client_info.ClientInfo() + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() __all__ = ("TranslationServiceAsyncClient",) diff --git a/google/cloud/translate_v3/services/translation_service/client.py b/google/cloud/translate_v3/services/translation_service/client.py index d00cd12d..af612268 100644 --- a/google/cloud/translate_v3/services/translation_service/client.py +++ b/google/cloud/translate_v3/services/translation_service/client.py @@ -16,27 +16,29 @@ # from collections import OrderedDict +from distutils import util import os import re -from typing import Callable, Dict, Sequence, Tuple, Type, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Type, Union import pkg_resources -import google.api_core.client_options as ClientOptions # type: ignore +from google.api_core import client_options as client_options_lib # type: ignore from google.api_core import exceptions # type: ignore from google.api_core import gapic_v1 # type: ignore from google.api_core import retry as retries # type: ignore from google.auth import credentials # type: ignore from google.auth.transport import mtls # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore from google.auth.exceptions import MutualTLSChannelError # type: ignore from google.oauth2 import service_account # type: ignore -from google.api_core import operation -from google.api_core import operation_async +from google.api_core import operation # type: ignore +from google.api_core import operation_async # type: ignore from google.cloud.translate_v3.services.translation_service import pagers from google.cloud.translate_v3.types import translation_service from google.protobuf import timestamp_pb2 as timestamp # type: ignore -from .transports.base import TranslationServiceTransport +from .transports.base import TranslationServiceTransport, DEFAULT_CLIENT_INFO from .transports.grpc import TranslationServiceGrpcTransport from .transports.grpc_asyncio import TranslationServiceGrpcAsyncIOTransport @@ -152,9 +154,10 @@ def parse_glossary_path(path: str) -> Dict[str, str]: def __init__( self, *, - credentials: credentials.Credentials = None, - transport: Union[str, TranslationServiceTransport] = None, - client_options: ClientOptions = None, + credentials: Optional[credentials.Credentials] = None, + transport: Union[str, TranslationServiceTransport, None] = None, + client_options: Optional[client_options_lib.ClientOptions] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: """Instantiate the translation service client. @@ -167,48 +170,74 @@ def __init__( transport (Union[str, ~.TranslationServiceTransport]): The transport to use. If set to None, a transport is chosen automatically. - client_options (ClientOptions): Custom options for the client. It - won't take effect if a ``transport`` instance is provided. + client_options (client_options_lib.ClientOptions): Custom options for the + client. It won't take effect if a ``transport`` instance is provided. (1) The ``api_endpoint`` property can be used to override the - default endpoint provided by the client. GOOGLE_API_USE_MTLS + default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT environment variable can also be used to override the endpoint: "always" (always use the default mTLS endpoint), "never" (always - use the default regular endpoint, this is the default value for - the environment variable) and "auto" (auto switch to the default - mTLS endpoint if client SSL credentials is present). However, - the ``api_endpoint`` property takes precedence if provided. - (2) The ``client_cert_source`` property is used to provide client - SSL credentials for mutual TLS transport. If not provided, the - default SSL credentials will be used if present. + use the default regular endpoint) and "auto" (auto switch to the + default mTLS endpoint if client certificate is present, this is + the default value). However, the ``api_endpoint`` property takes + precedence if provided. + (2) If GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide client certificate for mutual TLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport creation failed for any reason. """ if isinstance(client_options, dict): - client_options = ClientOptions.from_dict(client_options) + client_options = client_options_lib.from_dict(client_options) if client_options is None: - client_options = ClientOptions.ClientOptions() + client_options = client_options_lib.ClientOptions() - if client_options.api_endpoint is None: - use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS", "never") + # Create SSL credentials for mutual TLS if needed. + use_client_cert = bool( + util.strtobool(os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false")) + ) + + ssl_credentials = None + is_mtls = False + if use_client_cert: + if client_options.client_cert_source: + import grpc # type: ignore + + cert, key = client_options.client_cert_source() + ssl_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + is_mtls = True + else: + creds = SslCredentials() + is_mtls = creds.is_mtls + ssl_credentials = creds.ssl_credentials if is_mtls else None + + # Figure out which api endpoint to use. + if client_options.api_endpoint is not None: + api_endpoint = client_options.api_endpoint + else: + use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") if use_mtls_env == "never": - client_options.api_endpoint = self.DEFAULT_ENDPOINT + api_endpoint = self.DEFAULT_ENDPOINT elif use_mtls_env == "always": - client_options.api_endpoint = self.DEFAULT_MTLS_ENDPOINT + api_endpoint = self.DEFAULT_MTLS_ENDPOINT elif use_mtls_env == "auto": - has_client_cert_source = ( - client_options.client_cert_source is not None - or mtls.has_default_client_cert_source() - ) - client_options.api_endpoint = ( - self.DEFAULT_MTLS_ENDPOINT - if has_client_cert_source - else self.DEFAULT_ENDPOINT + api_endpoint = ( + self.DEFAULT_MTLS_ENDPOINT if is_mtls else self.DEFAULT_ENDPOINT ) else: raise MutualTLSChannelError( - "Unsupported GOOGLE_API_USE_MTLS value. Accepted values: never, auto, always" + "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted values: never, auto, always" ) # Save or instantiate the transport. @@ -232,11 +261,11 @@ def __init__( self._transport = Transport( credentials=credentials, credentials_file=client_options.credentials_file, - host=client_options.api_endpoint, + host=api_endpoint, scopes=client_options.scopes, - api_mtls_endpoint=client_options.api_endpoint, - client_cert_source=client_options.client_cert_source, + ssl_channel_credentials=ssl_credentials, quota_project_id=client_options.quota_project_id, + client_info=client_info, ) def translate_text( @@ -1056,11 +1085,11 @@ def delete_glossary( try: - _client_info = gapic_v1.client_info.ClientInfo( + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=pkg_resources.get_distribution("google-cloud-translate",).version, ) except pkg_resources.DistributionNotFound: - _client_info = gapic_v1.client_info.ClientInfo() + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() __all__ = ("TranslationServiceClient",) diff --git a/google/cloud/translate_v3/services/translation_service/transports/base.py b/google/cloud/translate_v3/services/translation_service/transports/base.py index ad022171..204e32ec 100644 --- a/google/cloud/translate_v3/services/translation_service/transports/base.py +++ b/google/cloud/translate_v3/services/translation_service/transports/base.py @@ -19,7 +19,7 @@ import typing import pkg_resources -from google import auth +from google import auth # type: ignore from google.api_core import exceptions # type: ignore from google.api_core import gapic_v1 # type: ignore from google.api_core import retry as retries # type: ignore @@ -31,11 +31,11 @@ try: - _client_info = gapic_v1.client_info.ClientInfo( + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=pkg_resources.get_distribution("google-cloud-translate",).version, ) except pkg_resources.DistributionNotFound: - _client_info = gapic_v1.client_info.ClientInfo() + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() class TranslationServiceTransport(abc.ABC): @@ -54,6 +54,7 @@ def __init__( credentials_file: typing.Optional[str] = None, scopes: typing.Optional[typing.Sequence[str]] = AUTH_SCOPES, quota_project_id: typing.Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, **kwargs, ) -> None: """Instantiate the transport. @@ -71,6 +72,11 @@ def __init__( scope (Optional[Sequence[str]]): A list of scopes. quota_project_id (Optional[str]): An optional project to use for billing and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. """ # Save the hostname. Default to port 443 (HTTPS) if none is specified. if ":" not in host: @@ -98,16 +104,16 @@ def __init__( self._credentials = credentials # Lifted into its own function so it can be stubbed out during tests. - self._prep_wrapped_messages() + self._prep_wrapped_messages(client_info) - def _prep_wrapped_messages(self): + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. self._wrapped_methods = { self.translate_text: gapic_v1.method.wrap_method( - self.translate_text, default_timeout=600.0, client_info=_client_info, + self.translate_text, default_timeout=600.0, client_info=client_info, ), self.detect_language: gapic_v1.method.wrap_method( - self.detect_language, default_timeout=600.0, client_info=_client_info, + self.detect_language, default_timeout=600.0, client_info=client_info, ), self.get_supported_languages: gapic_v1.method.wrap_method( self.get_supported_languages, @@ -120,15 +126,15 @@ def _prep_wrapped_messages(self): ), ), default_timeout=600.0, - client_info=_client_info, + client_info=client_info, ), self.batch_translate_text: gapic_v1.method.wrap_method( self.batch_translate_text, default_timeout=600.0, - client_info=_client_info, + client_info=client_info, ), self.create_glossary: gapic_v1.method.wrap_method( - self.create_glossary, default_timeout=600.0, client_info=_client_info, + self.create_glossary, default_timeout=600.0, client_info=client_info, ), self.list_glossaries: gapic_v1.method.wrap_method( self.list_glossaries, @@ -141,7 +147,7 @@ def _prep_wrapped_messages(self): ), ), default_timeout=600.0, - client_info=_client_info, + client_info=client_info, ), self.get_glossary: gapic_v1.method.wrap_method( self.get_glossary, @@ -154,7 +160,7 @@ def _prep_wrapped_messages(self): ), ), default_timeout=600.0, - client_info=_client_info, + client_info=client_info, ), self.delete_glossary: gapic_v1.method.wrap_method( self.delete_glossary, @@ -167,7 +173,7 @@ def _prep_wrapped_messages(self): ), ), default_timeout=600.0, - client_info=_client_info, + client_info=client_info, ), } diff --git a/google/cloud/translate_v3/services/translation_service/transports/grpc.py b/google/cloud/translate_v3/services/translation_service/transports/grpc.py index f8ee5ef5..09e06595 100644 --- a/google/cloud/translate_v3/services/translation_service/transports/grpc.py +++ b/google/cloud/translate_v3/services/translation_service/transports/grpc.py @@ -15,21 +15,22 @@ # limitations under the License. # +import warnings from typing import Callable, Dict, Optional, Sequence, Tuple from google.api_core import grpc_helpers # type: ignore from google.api_core import operations_v1 # type: ignore +from google.api_core import gapic_v1 # type: ignore from google import auth # type: ignore from google.auth import credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore - import grpc # type: ignore from google.cloud.translate_v3.types import translation_service from google.longrunning import operations_pb2 as operations # type: ignore -from .base import TranslationServiceTransport +from .base import TranslationServiceTransport, DEFAULT_CLIENT_INFO class TranslationServiceGrpcTransport(TranslationServiceTransport): @@ -57,7 +58,9 @@ def __init__( channel: grpc.Channel = None, api_mtls_endpoint: str = None, client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, - quota_project_id: Optional[str] = None + ssl_channel_credentials: grpc.ChannelCredentials = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: """Instantiate the transport. @@ -76,16 +79,23 @@ def __init__( ignored if ``channel`` is provided. channel (Optional[grpc.Channel]): A ``Channel`` instance through which to make calls. - api_mtls_endpoint (Optional[str]): The mutual TLS endpoint. If - provided, it overrides the ``host`` argument and tries to create + api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. + If provided, it overrides the ``host`` argument and tries to create a mutual TLS channel with client SSL credentials from ``client_cert_source`` or applicatin default SSL credentials. - client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): A - callback to provide client SSL certificate bytes and private key - bytes, both in PEM format. It is ignored if ``api_mtls_endpoint`` - is None. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + Deprecated. A callback to provide client SSL certificate bytes and + private key bytes, both in PEM format. It is ignored if + ``api_mtls_endpoint`` is None. + ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials + for grpc channel. It is ignored if ``channel`` is provided. quota_project_id (Optional[str]): An optional project to use for billing and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -101,6 +111,11 @@ def __init__( # If a channel was explicitly provided, set it. self._grpc_channel = channel elif api_mtls_endpoint: + warnings.warn( + "api_mtls_endpoint and client_cert_source are deprecated", + DeprecationWarning, + ) + host = ( api_mtls_endpoint if ":" in api_mtls_endpoint @@ -131,6 +146,23 @@ def __init__( scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, ) + else: + host = host if ":" in host else host + ":443" + + if credentials is None: + credentials, _ = auth.default( + scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id + ) + + # create a new channel. The provided one is ignored. + self._grpc_channel = type(self).create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + ssl_credentials=ssl_channel_credentials, + scopes=scopes or self.AUTH_SCOPES, + quota_project_id=quota_project_id, + ) self._stubs = {} # type: Dict[str, Callable] @@ -141,6 +173,7 @@ def __init__( credentials_file=credentials_file, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, + client_info=client_info, ) @classmethod @@ -151,7 +184,7 @@ def create_channel( credentials_file: str = None, scopes: Optional[Sequence[str]] = None, quota_project_id: Optional[str] = None, - **kwargs + **kwargs, ) -> grpc.Channel: """Create and return a gRPC channel object. Args: @@ -185,7 +218,7 @@ def create_channel( credentials_file=credentials_file, scopes=scopes, quota_project_id=quota_project_id, - **kwargs + **kwargs, ) @property @@ -195,13 +228,6 @@ def grpc_channel(self) -> grpc.Channel: This property caches on the instance; repeated calls return the same channel. """ - # Sanity check: Only create a new channel if we do not already - # have one. - if not hasattr(self, "_grpc_channel"): - self._grpc_channel = self.create_channel( - self._host, credentials=self._credentials, - ) - # Return the channel from cache. return self._grpc_channel diff --git a/google/cloud/translate_v3/services/translation_service/transports/grpc_asyncio.py b/google/cloud/translate_v3/services/translation_service/transports/grpc_asyncio.py index 7fb7d890..e7e9c05c 100644 --- a/google/cloud/translate_v3/services/translation_service/transports/grpc_asyncio.py +++ b/google/cloud/translate_v3/services/translation_service/transports/grpc_asyncio.py @@ -15,10 +15,13 @@ # limitations under the License. # +import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple +from google.api_core import gapic_v1 # type: ignore from google.api_core import grpc_helpers_async # type: ignore from google.api_core import operations_v1 # type: ignore +from google import auth # type: ignore from google.auth import credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore @@ -28,7 +31,7 @@ from google.cloud.translate_v3.types import translation_service from google.longrunning import operations_pb2 as operations # type: ignore -from .base import TranslationServiceTransport +from .base import TranslationServiceTransport, DEFAULT_CLIENT_INFO from .grpc import TranslationServiceGrpcTransport @@ -99,7 +102,9 @@ def __init__( channel: aio.Channel = None, api_mtls_endpoint: str = None, client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, + ssl_channel_credentials: grpc.ChannelCredentials = None, quota_project_id=None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: """Instantiate the transport. @@ -119,16 +124,23 @@ def __init__( are passed to :func:`google.auth.default`. channel (Optional[aio.Channel]): A ``Channel`` instance through which to make calls. - api_mtls_endpoint (Optional[str]): The mutual TLS endpoint. If - provided, it overrides the ``host`` argument and tries to create + api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. + If provided, it overrides the ``host`` argument and tries to create a mutual TLS channel with client SSL credentials from ``client_cert_source`` or applicatin default SSL credentials. - client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): A - callback to provide client SSL certificate bytes and private key - bytes, both in PEM format. It is ignored if ``api_mtls_endpoint`` - is None. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + Deprecated. A callback to provide client SSL certificate bytes and + private key bytes, both in PEM format. It is ignored if + ``api_mtls_endpoint`` is None. + ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials + for grpc channel. It is ignored if ``channel`` is provided. quota_project_id (Optional[str]): An optional project to use for billing and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -144,12 +156,22 @@ def __init__( # If a channel was explicitly provided, set it. self._grpc_channel = channel elif api_mtls_endpoint: + warnings.warn( + "api_mtls_endpoint and client_cert_source are deprecated", + DeprecationWarning, + ) + host = ( api_mtls_endpoint if ":" in api_mtls_endpoint else api_mtls_endpoint + ":443" ) + if credentials is None: + credentials, _ = auth.default( + scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id + ) + # Create SSL credentials with client_cert_source or application # default SSL credentials. if client_cert_source: @@ -169,6 +191,23 @@ def __init__( scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, ) + else: + host = host if ":" in host else host + ":443" + + if credentials is None: + credentials, _ = auth.default( + scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id + ) + + # create a new channel. The provided one is ignored. + self._grpc_channel = type(self).create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + ssl_credentials=ssl_channel_credentials, + scopes=scopes or self.AUTH_SCOPES, + quota_project_id=quota_project_id, + ) # Run the base constructor. super().__init__( @@ -177,6 +216,7 @@ def __init__( credentials_file=credentials_file, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, + client_info=client_info, ) self._stubs = {} @@ -188,13 +228,6 @@ def grpc_channel(self) -> aio.Channel: This property caches on the instance; repeated calls return the same channel. """ - # Sanity check: Only create a new channel if we do not already - # have one. - if not hasattr(self, "_grpc_channel"): - self._grpc_channel = self.create_channel( - self._host, credentials=self._credentials, - ) - # Return the channel from cache. return self._grpc_channel diff --git a/google/cloud/translate_v3beta1/services/translation_service/async_client.py b/google/cloud/translate_v3beta1/services/translation_service/async_client.py index fe0e508b..9054a09b 100644 --- a/google/cloud/translate_v3beta1/services/translation_service/async_client.py +++ b/google/cloud/translate_v3beta1/services/translation_service/async_client.py @@ -28,13 +28,13 @@ from google.auth import credentials # type: ignore from google.oauth2 import service_account # type: ignore -from google.api_core import operation -from google.api_core import operation_async +from google.api_core import operation # type: ignore +from google.api_core import operation_async # type: ignore from google.cloud.translate_v3beta1.services.translation_service import pagers from google.cloud.translate_v3beta1.types import translation_service from google.protobuf import timestamp_pb2 as timestamp # type: ignore -from .transports.base import TranslationServiceTransport +from .transports.base import TranslationServiceTransport, DEFAULT_CLIENT_INFO from .transports.grpc_asyncio import TranslationServiceGrpcAsyncIOTransport from .client import TranslationServiceClient @@ -48,6 +48,7 @@ class TranslationServiceAsyncClient: DEFAULT_MTLS_ENDPOINT = TranslationServiceClient.DEFAULT_MTLS_ENDPOINT glossary_path = staticmethod(TranslationServiceClient.glossary_path) + parse_glossary_path = staticmethod(TranslationServiceClient.parse_glossary_path) from_service_account_file = TranslationServiceClient.from_service_account_file from_service_account_json = from_service_account_file @@ -63,6 +64,7 @@ def __init__( credentials: credentials.Credentials = None, transport: Union[str, TranslationServiceTransport] = "grpc_asyncio", client_options: ClientOptions = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: """Instantiate the translation service client. @@ -78,16 +80,19 @@ def __init__( client_options (ClientOptions): Custom options for the client. It won't take effect if a ``transport`` instance is provided. (1) The ``api_endpoint`` property can be used to override the - default endpoint provided by the client. GOOGLE_API_USE_MTLS + default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT environment variable can also be used to override the endpoint: "always" (always use the default mTLS endpoint), "never" (always - use the default regular endpoint, this is the default value for - the environment variable) and "auto" (auto switch to the default - mTLS endpoint if client SSL credentials is present). However, - the ``api_endpoint`` property takes precedence if provided. - (2) The ``client_cert_source`` property is used to provide client - SSL credentials for mutual TLS transport. If not provided, the - default SSL credentials will be used if present. + use the default regular endpoint) and "auto" (auto switch to the + default mTLS endpoint if client certificate is present, this is + the default value). However, the ``api_endpoint`` property takes + precedence if provided. + (2) If GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide client certificate for mutual TLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -95,7 +100,10 @@ def __init__( """ self._client = TranslationServiceClient( - credentials=credentials, transport=transport, client_options=client_options, + credentials=credentials, + transport=transport, + client_options=client_options, + client_info=client_info, ) async def translate_text( @@ -132,7 +140,7 @@ async def translate_text( rpc = gapic_v1.method_async.wrap_method( self._client._transport.translate_text, default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -243,7 +251,7 @@ async def detect_language( rpc = gapic_v1.method_async.wrap_method( self._client._transport.detect_language, default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -365,11 +373,11 @@ async def get_supported_languages( maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, ), ), default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -433,7 +441,7 @@ async def batch_translate_text( rpc = gapic_v1.method_async.wrap_method( self._client._transport.batch_translate_text, default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -522,7 +530,7 @@ async def create_glossary( rpc = gapic_v1.method_async.wrap_method( self._client._transport.create_glossary, default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -620,11 +628,11 @@ async def list_glossaries( maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, ), ), default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -705,11 +713,11 @@ async def get_glossary( maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, ), ), default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -790,11 +798,11 @@ async def delete_glossary( maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, ), ), default_timeout=600.0, - client_info=_client_info, + client_info=DEFAULT_CLIENT_INFO, ) # Certain fields should be provided within the metadata header; @@ -819,11 +827,11 @@ async def delete_glossary( try: - _client_info = gapic_v1.client_info.ClientInfo( + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=pkg_resources.get_distribution("google-cloud-translate",).version, ) except pkg_resources.DistributionNotFound: - _client_info = gapic_v1.client_info.ClientInfo() + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() __all__ = ("TranslationServiceAsyncClient",) diff --git a/google/cloud/translate_v3beta1/services/translation_service/client.py b/google/cloud/translate_v3beta1/services/translation_service/client.py index 6d991415..47a82cca 100644 --- a/google/cloud/translate_v3beta1/services/translation_service/client.py +++ b/google/cloud/translate_v3beta1/services/translation_service/client.py @@ -16,27 +16,29 @@ # from collections import OrderedDict +from distutils import util import os import re -from typing import Callable, Dict, Sequence, Tuple, Type, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Type, Union import pkg_resources -import google.api_core.client_options as ClientOptions # type: ignore +from google.api_core import client_options as client_options_lib # type: ignore from google.api_core import exceptions # type: ignore from google.api_core import gapic_v1 # type: ignore from google.api_core import retry as retries # type: ignore from google.auth import credentials # type: ignore from google.auth.transport import mtls # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore from google.auth.exceptions import MutualTLSChannelError # type: ignore from google.oauth2 import service_account # type: ignore -from google.api_core import operation -from google.api_core import operation_async +from google.api_core import operation # type: ignore +from google.api_core import operation_async # type: ignore from google.cloud.translate_v3beta1.services.translation_service import pagers from google.cloud.translate_v3beta1.types import translation_service from google.protobuf import timestamp_pb2 as timestamp # type: ignore -from .transports.base import TranslationServiceTransport +from .transports.base import TranslationServiceTransport, DEFAULT_CLIENT_INFO from .transports.grpc import TranslationServiceGrpcTransport from .transports.grpc_asyncio import TranslationServiceGrpcAsyncIOTransport @@ -152,9 +154,10 @@ def parse_glossary_path(path: str) -> Dict[str, str]: def __init__( self, *, - credentials: credentials.Credentials = None, - transport: Union[str, TranslationServiceTransport] = None, - client_options: ClientOptions = None, + credentials: Optional[credentials.Credentials] = None, + transport: Union[str, TranslationServiceTransport, None] = None, + client_options: Optional[client_options_lib.ClientOptions] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: """Instantiate the translation service client. @@ -167,48 +170,74 @@ def __init__( transport (Union[str, ~.TranslationServiceTransport]): The transport to use. If set to None, a transport is chosen automatically. - client_options (ClientOptions): Custom options for the client. It - won't take effect if a ``transport`` instance is provided. + client_options (client_options_lib.ClientOptions): Custom options for the + client. It won't take effect if a ``transport`` instance is provided. (1) The ``api_endpoint`` property can be used to override the - default endpoint provided by the client. GOOGLE_API_USE_MTLS + default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT environment variable can also be used to override the endpoint: "always" (always use the default mTLS endpoint), "never" (always - use the default regular endpoint, this is the default value for - the environment variable) and "auto" (auto switch to the default - mTLS endpoint if client SSL credentials is present). However, - the ``api_endpoint`` property takes precedence if provided. - (2) The ``client_cert_source`` property is used to provide client - SSL credentials for mutual TLS transport. If not provided, the - default SSL credentials will be used if present. + use the default regular endpoint) and "auto" (auto switch to the + default mTLS endpoint if client certificate is present, this is + the default value). However, the ``api_endpoint`` property takes + precedence if provided. + (2) If GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide client certificate for mutual TLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport creation failed for any reason. """ if isinstance(client_options, dict): - client_options = ClientOptions.from_dict(client_options) + client_options = client_options_lib.from_dict(client_options) if client_options is None: - client_options = ClientOptions.ClientOptions() + client_options = client_options_lib.ClientOptions() - if client_options.api_endpoint is None: - use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS", "never") + # Create SSL credentials for mutual TLS if needed. + use_client_cert = bool( + util.strtobool(os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false")) + ) + + ssl_credentials = None + is_mtls = False + if use_client_cert: + if client_options.client_cert_source: + import grpc # type: ignore + + cert, key = client_options.client_cert_source() + ssl_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + is_mtls = True + else: + creds = SslCredentials() + is_mtls = creds.is_mtls + ssl_credentials = creds.ssl_credentials if is_mtls else None + + # Figure out which api endpoint to use. + if client_options.api_endpoint is not None: + api_endpoint = client_options.api_endpoint + else: + use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") if use_mtls_env == "never": - client_options.api_endpoint = self.DEFAULT_ENDPOINT + api_endpoint = self.DEFAULT_ENDPOINT elif use_mtls_env == "always": - client_options.api_endpoint = self.DEFAULT_MTLS_ENDPOINT + api_endpoint = self.DEFAULT_MTLS_ENDPOINT elif use_mtls_env == "auto": - has_client_cert_source = ( - client_options.client_cert_source is not None - or mtls.has_default_client_cert_source() - ) - client_options.api_endpoint = ( - self.DEFAULT_MTLS_ENDPOINT - if has_client_cert_source - else self.DEFAULT_ENDPOINT + api_endpoint = ( + self.DEFAULT_MTLS_ENDPOINT if is_mtls else self.DEFAULT_ENDPOINT ) else: raise MutualTLSChannelError( - "Unsupported GOOGLE_API_USE_MTLS value. Accepted values: never, auto, always" + "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted values: never, auto, always" ) # Save or instantiate the transport. @@ -232,11 +261,11 @@ def __init__( self._transport = Transport( credentials=credentials, credentials_file=client_options.credentials_file, - host=client_options.api_endpoint, + host=api_endpoint, scopes=client_options.scopes, - api_mtls_endpoint=client_options.api_endpoint, - client_cert_source=client_options.client_cert_source, + ssl_channel_credentials=ssl_credentials, quota_project_id=client_options.quota_project_id, + client_info=client_info, ) def translate_text( @@ -942,11 +971,11 @@ def delete_glossary( try: - _client_info = gapic_v1.client_info.ClientInfo( + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=pkg_resources.get_distribution("google-cloud-translate",).version, ) except pkg_resources.DistributionNotFound: - _client_info = gapic_v1.client_info.ClientInfo() + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() __all__ = ("TranslationServiceClient",) diff --git a/google/cloud/translate_v3beta1/services/translation_service/transports/base.py b/google/cloud/translate_v3beta1/services/translation_service/transports/base.py index b532a056..7c9f6810 100644 --- a/google/cloud/translate_v3beta1/services/translation_service/transports/base.py +++ b/google/cloud/translate_v3beta1/services/translation_service/transports/base.py @@ -19,7 +19,7 @@ import typing import pkg_resources -from google import auth +from google import auth # type: ignore from google.api_core import exceptions # type: ignore from google.api_core import gapic_v1 # type: ignore from google.api_core import retry as retries # type: ignore @@ -31,11 +31,11 @@ try: - _client_info = gapic_v1.client_info.ClientInfo( + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=pkg_resources.get_distribution("google-cloud-translate",).version, ) except pkg_resources.DistributionNotFound: - _client_info = gapic_v1.client_info.ClientInfo() + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() class TranslationServiceTransport(abc.ABC): @@ -54,6 +54,7 @@ def __init__( credentials_file: typing.Optional[str] = None, scopes: typing.Optional[typing.Sequence[str]] = AUTH_SCOPES, quota_project_id: typing.Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, **kwargs, ) -> None: """Instantiate the transport. @@ -71,6 +72,11 @@ def __init__( scope (Optional[Sequence[str]]): A list of scopes. quota_project_id (Optional[str]): An optional project to use for billing and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. """ # Save the hostname. Default to port 443 (HTTPS) if none is specified. if ":" not in host: @@ -98,16 +104,16 @@ def __init__( self._credentials = credentials # Lifted into its own function so it can be stubbed out during tests. - self._prep_wrapped_messages() + self._prep_wrapped_messages(client_info) - def _prep_wrapped_messages(self): + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. self._wrapped_methods = { self.translate_text: gapic_v1.method.wrap_method( - self.translate_text, default_timeout=600.0, client_info=_client_info, + self.translate_text, default_timeout=600.0, client_info=client_info, ), self.detect_language: gapic_v1.method.wrap_method( - self.detect_language, default_timeout=600.0, client_info=_client_info, + self.detect_language, default_timeout=600.0, client_info=client_info, ), self.get_supported_languages: gapic_v1.method.wrap_method( self.get_supported_languages, @@ -116,19 +122,19 @@ def _prep_wrapped_messages(self): maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, ), ), default_timeout=600.0, - client_info=_client_info, + client_info=client_info, ), self.batch_translate_text: gapic_v1.method.wrap_method( self.batch_translate_text, default_timeout=600.0, - client_info=_client_info, + client_info=client_info, ), self.create_glossary: gapic_v1.method.wrap_method( - self.create_glossary, default_timeout=600.0, client_info=_client_info, + self.create_glossary, default_timeout=600.0, client_info=client_info, ), self.list_glossaries: gapic_v1.method.wrap_method( self.list_glossaries, @@ -137,11 +143,11 @@ def _prep_wrapped_messages(self): maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, ), ), default_timeout=600.0, - client_info=_client_info, + client_info=client_info, ), self.get_glossary: gapic_v1.method.wrap_method( self.get_glossary, @@ -150,11 +156,11 @@ def _prep_wrapped_messages(self): maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, ), ), default_timeout=600.0, - client_info=_client_info, + client_info=client_info, ), self.delete_glossary: gapic_v1.method.wrap_method( self.delete_glossary, @@ -163,11 +169,11 @@ def _prep_wrapped_messages(self): maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, ), ), default_timeout=600.0, - client_info=_client_info, + client_info=client_info, ), } diff --git a/google/cloud/translate_v3beta1/services/translation_service/transports/grpc.py b/google/cloud/translate_v3beta1/services/translation_service/transports/grpc.py index f6dadf28..88882482 100644 --- a/google/cloud/translate_v3beta1/services/translation_service/transports/grpc.py +++ b/google/cloud/translate_v3beta1/services/translation_service/transports/grpc.py @@ -15,21 +15,22 @@ # limitations under the License. # +import warnings from typing import Callable, Dict, Optional, Sequence, Tuple from google.api_core import grpc_helpers # type: ignore from google.api_core import operations_v1 # type: ignore +from google.api_core import gapic_v1 # type: ignore from google import auth # type: ignore from google.auth import credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore - import grpc # type: ignore from google.cloud.translate_v3beta1.types import translation_service from google.longrunning import operations_pb2 as operations # type: ignore -from .base import TranslationServiceTransport +from .base import TranslationServiceTransport, DEFAULT_CLIENT_INFO class TranslationServiceGrpcTransport(TranslationServiceTransport): @@ -57,7 +58,9 @@ def __init__( channel: grpc.Channel = None, api_mtls_endpoint: str = None, client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, - quota_project_id: Optional[str] = None + ssl_channel_credentials: grpc.ChannelCredentials = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: """Instantiate the transport. @@ -76,16 +79,23 @@ def __init__( ignored if ``channel`` is provided. channel (Optional[grpc.Channel]): A ``Channel`` instance through which to make calls. - api_mtls_endpoint (Optional[str]): The mutual TLS endpoint. If - provided, it overrides the ``host`` argument and tries to create + api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. + If provided, it overrides the ``host`` argument and tries to create a mutual TLS channel with client SSL credentials from ``client_cert_source`` or applicatin default SSL credentials. - client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): A - callback to provide client SSL certificate bytes and private key - bytes, both in PEM format. It is ignored if ``api_mtls_endpoint`` - is None. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + Deprecated. A callback to provide client SSL certificate bytes and + private key bytes, both in PEM format. It is ignored if + ``api_mtls_endpoint`` is None. + ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials + for grpc channel. It is ignored if ``channel`` is provided. quota_project_id (Optional[str]): An optional project to use for billing and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -101,6 +111,11 @@ def __init__( # If a channel was explicitly provided, set it. self._grpc_channel = channel elif api_mtls_endpoint: + warnings.warn( + "api_mtls_endpoint and client_cert_source are deprecated", + DeprecationWarning, + ) + host = ( api_mtls_endpoint if ":" in api_mtls_endpoint @@ -131,6 +146,23 @@ def __init__( scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, ) + else: + host = host if ":" in host else host + ":443" + + if credentials is None: + credentials, _ = auth.default( + scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id + ) + + # create a new channel. The provided one is ignored. + self._grpc_channel = type(self).create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + ssl_credentials=ssl_channel_credentials, + scopes=scopes or self.AUTH_SCOPES, + quota_project_id=quota_project_id, + ) self._stubs = {} # type: Dict[str, Callable] @@ -141,6 +173,7 @@ def __init__( credentials_file=credentials_file, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, + client_info=client_info, ) @classmethod @@ -151,7 +184,7 @@ def create_channel( credentials_file: str = None, scopes: Optional[Sequence[str]] = None, quota_project_id: Optional[str] = None, - **kwargs + **kwargs, ) -> grpc.Channel: """Create and return a gRPC channel object. Args: @@ -185,7 +218,7 @@ def create_channel( credentials_file=credentials_file, scopes=scopes, quota_project_id=quota_project_id, - **kwargs + **kwargs, ) @property @@ -195,13 +228,6 @@ def grpc_channel(self) -> grpc.Channel: This property caches on the instance; repeated calls return the same channel. """ - # Sanity check: Only create a new channel if we do not already - # have one. - if not hasattr(self, "_grpc_channel"): - self._grpc_channel = self.create_channel( - self._host, credentials=self._credentials, - ) - # Return the channel from cache. return self._grpc_channel diff --git a/google/cloud/translate_v3beta1/services/translation_service/transports/grpc_asyncio.py b/google/cloud/translate_v3beta1/services/translation_service/transports/grpc_asyncio.py index e7f63e99..883c06a6 100644 --- a/google/cloud/translate_v3beta1/services/translation_service/transports/grpc_asyncio.py +++ b/google/cloud/translate_v3beta1/services/translation_service/transports/grpc_asyncio.py @@ -15,10 +15,13 @@ # limitations under the License. # +import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple +from google.api_core import gapic_v1 # type: ignore from google.api_core import grpc_helpers_async # type: ignore from google.api_core import operations_v1 # type: ignore +from google import auth # type: ignore from google.auth import credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore @@ -28,7 +31,7 @@ from google.cloud.translate_v3beta1.types import translation_service from google.longrunning import operations_pb2 as operations # type: ignore -from .base import TranslationServiceTransport +from .base import TranslationServiceTransport, DEFAULT_CLIENT_INFO from .grpc import TranslationServiceGrpcTransport @@ -99,7 +102,9 @@ def __init__( channel: aio.Channel = None, api_mtls_endpoint: str = None, client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, + ssl_channel_credentials: grpc.ChannelCredentials = None, quota_project_id=None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: """Instantiate the transport. @@ -119,16 +124,23 @@ def __init__( are passed to :func:`google.auth.default`. channel (Optional[aio.Channel]): A ``Channel`` instance through which to make calls. - api_mtls_endpoint (Optional[str]): The mutual TLS endpoint. If - provided, it overrides the ``host`` argument and tries to create + api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. + If provided, it overrides the ``host`` argument and tries to create a mutual TLS channel with client SSL credentials from ``client_cert_source`` or applicatin default SSL credentials. - client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): A - callback to provide client SSL certificate bytes and private key - bytes, both in PEM format. It is ignored if ``api_mtls_endpoint`` - is None. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + Deprecated. A callback to provide client SSL certificate bytes and + private key bytes, both in PEM format. It is ignored if + ``api_mtls_endpoint`` is None. + ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials + for grpc channel. It is ignored if ``channel`` is provided. quota_project_id (Optional[str]): An optional project to use for billing and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -144,12 +156,22 @@ def __init__( # If a channel was explicitly provided, set it. self._grpc_channel = channel elif api_mtls_endpoint: + warnings.warn( + "api_mtls_endpoint and client_cert_source are deprecated", + DeprecationWarning, + ) + host = ( api_mtls_endpoint if ":" in api_mtls_endpoint else api_mtls_endpoint + ":443" ) + if credentials is None: + credentials, _ = auth.default( + scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id + ) + # Create SSL credentials with client_cert_source or application # default SSL credentials. if client_cert_source: @@ -169,6 +191,23 @@ def __init__( scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, ) + else: + host = host if ":" in host else host + ":443" + + if credentials is None: + credentials, _ = auth.default( + scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id + ) + + # create a new channel. The provided one is ignored. + self._grpc_channel = type(self).create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + ssl_credentials=ssl_channel_credentials, + scopes=scopes or self.AUTH_SCOPES, + quota_project_id=quota_project_id, + ) # Run the base constructor. super().__init__( @@ -177,6 +216,7 @@ def __init__( credentials_file=credentials_file, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, + client_info=client_info, ) self._stubs = {} @@ -188,13 +228,6 @@ def grpc_channel(self) -> aio.Channel: This property caches on the instance; repeated calls return the same channel. """ - # Sanity check: Only create a new channel if we do not already - # have one. - if not hasattr(self, "_grpc_channel"): - self._grpc_channel = self.create_channel( - self._host, credentials=self._credentials, - ) - # Return the channel from cache. return self._grpc_channel diff --git a/noxfile.py b/noxfile.py index 86bba293..845cd5f9 100644 --- a/noxfile.py +++ b/noxfile.py @@ -173,7 +173,9 @@ def docfx(session): """Build the docfx yaml files for this library.""" session.install("-e", ".") - session.install("sphinx", "alabaster", "recommonmark", "sphinx-docfx-yaml") + # sphinx-docfx-yaml supports up to sphinx version 1.5.5. + # https://github.com/docascode/sphinx-docfx-yaml/issues/97 + session.install("sphinx==1.5.5", "alabaster", "recommonmark", "sphinx-docfx-yaml") shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) session.run( diff --git a/samples/snippets/hybrid_glossaries/noxfile.py b/samples/snippets/hybrid_glossaries/noxfile.py index 5660f08b..f3a90583 100644 --- a/samples/snippets/hybrid_glossaries/noxfile.py +++ b/samples/snippets/hybrid_glossaries/noxfile.py @@ -199,6 +199,11 @@ def _get_repo_root(): break if Path(p / ".git").exists(): return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) p = p.parent raise Exception("Unable to detect repository root.") diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index 5660f08b..27d948d6 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -180,9 +180,9 @@ def py(session): if session.python in TESTED_VERSIONS: _session_tests(session) else: - session.skip( - "SKIPPED: {} tests are disabled for this sample.".format(session.python) - ) + session.skip("SKIPPED: {} tests are disabled for this sample.".format( + session.python + )) # @@ -199,6 +199,11 @@ def _get_repo_root(): break if Path(p / ".git").exists(): return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) p = p.parent raise Exception("Unable to detect repository root.") diff --git a/scripts/decrypt-secrets.sh b/scripts/decrypt-secrets.sh index ff599eb2..21f6d2a2 100755 --- a/scripts/decrypt-secrets.sh +++ b/scripts/decrypt-secrets.sh @@ -20,14 +20,27 @@ ROOT=$( dirname "$DIR" ) # Work from the project root. cd $ROOT +# Prevent it from overriding files. +# We recommend that sample authors use their own service account files and cloud project. +# In that case, they are supposed to prepare these files by themselves. +if [[ -f "testing/test-env.sh" ]] || \ + [[ -f "testing/service-account.json" ]] || \ + [[ -f "testing/client-secrets.json" ]]; then + echo "One or more target files exist, aborting." + exit 1 +fi + # Use SECRET_MANAGER_PROJECT if set, fallback to cloud-devrel-kokoro-resources. PROJECT_ID="${SECRET_MANAGER_PROJECT:-cloud-devrel-kokoro-resources}" gcloud secrets versions access latest --secret="python-docs-samples-test-env" \ + --project="${PROJECT_ID}" \ > testing/test-env.sh gcloud secrets versions access latest \ --secret="python-docs-samples-service-account" \ + --project="${PROJECT_ID}" \ > testing/service-account.json gcloud secrets versions access latest \ --secret="python-docs-samples-client-secrets" \ - > testing/client-secrets.json \ No newline at end of file + --project="${PROJECT_ID}" \ + > testing/client-secrets.json diff --git a/synth.metadata b/synth.metadata index 32b3b56a..85cf89ed 100644 --- a/synth.metadata +++ b/synth.metadata @@ -3,22 +3,22 @@ { "git": { "name": ".", - "remote": "git@github.com:danoscarmike/python-translate", - "sha": "2a80f87c6ff441fb81161db8cda1049aa6851cc4" + "remote": "https://github.com/googleapis/python-translate.git", + "sha": "2bc6296122e54bc93804d37411ff7554e2808626" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "5f2f711c91199ba2f609d3f06a2fe22aee4e5be3" + "sha": "f68649c5f26bcff6817c6d21e90dac0fc71fef8e" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "5f2f711c91199ba2f609d3f06a2fe22aee4e5be3" + "sha": "f68649c5f26bcff6817c6d21e90dac0fc71fef8e" } } ], diff --git a/tests/system.py b/tests/system.py index 56b16091..0cf45215 100644 --- a/tests/system.py +++ b/tests/system.py @@ -15,6 +15,7 @@ import os +import pytest import unittest from google.cloud import translate_v2 @@ -32,6 +33,7 @@ class Config(object): CLIENT_V3 = None location = "global" project_id = os.environ["PROJECT_ID"] + use_mtls = os.environ.get("GOOGLE_API_USE_MTLS_ENDPOINT", "never") def setUpModule(): @@ -39,7 +41,15 @@ def setUpModule(): Config.CLIENT_V3 = translate.TranslationServiceClient() +# Only v3/v3beta1 clients have mTLS support, so we need to skip all the +# v2 client tests for mTLS testing. +skip_for_mtls = pytest.mark.skipif( + Config.use_mtls == "always", reason="Skip the v2 client test for mTLS testing" +) + + class TestTranslate(unittest.TestCase): + @skip_for_mtls def test_get_languages(self): result = Config.CLIENT_V2.get_languages() # There are **many** more than 10 languages. @@ -51,6 +61,7 @@ def test_get_languages(self): self.assertEqual(lang_map["lv"], "Latvian") self.assertEqual(lang_map["zu"], "Zulu") + @skip_for_mtls def test_detect_language(self): values = ["takoy", "fa\xe7ade", "s'il vous plait"] detections = Config.CLIENT_V2.detect_language(values) @@ -59,6 +70,7 @@ def test_detect_language(self): self.assertEqual(detections[1]["language"], "fr") self.assertEqual(detections[2]["language"], "fr") + @skip_for_mtls def test_translate(self): values = ["petnaest", "dek kvin", "Me llamo Jeff", "My name is Jeff"] translations = Config.CLIENT_V2.translate( diff --git a/tests/unit/gapic/translate_v3/test_translation_service.py b/tests/unit/gapic/translate_v3/test_translation_service.py index 556a93a2..c296227c 100644 --- a/tests/unit/gapic/translate_v3/test_translation_service.py +++ b/tests/unit/gapic/translate_v3/test_translation_service.py @@ -31,7 +31,7 @@ from google.api_core import gapic_v1 from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async -from google.api_core import operation_async +from google.api_core import operation_async # type: ignore from google.api_core import operations_v1 from google.auth import credentials from google.auth.exceptions import MutualTLSChannelError @@ -165,14 +165,14 @@ def test_translation_service_client_client_options( credentials_file=None, host="squid.clam.whelk", scopes=None, - api_mtls_endpoint="squid.clam.whelk", - client_cert_source=None, + ssl_channel_credentials=None, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) - # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS is + # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT is # "never". - with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS": "never"}): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None client = client_class() @@ -181,14 +181,14 @@ def test_translation_service_client_client_options( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=None, - api_mtls_endpoint=client.DEFAULT_ENDPOINT, - client_cert_source=None, + ssl_channel_credentials=None, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) - # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS is + # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT is # "always". - with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS": "always"}): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None client = client_class() @@ -197,90 +197,185 @@ def test_translation_service_client_client_options( credentials_file=None, host=client.DEFAULT_MTLS_ENDPOINT, scopes=None, - api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT, - client_cert_source=None, + ssl_channel_credentials=None, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) - # Check the case api_endpoint is not provided, GOOGLE_API_USE_MTLS is - # "auto", and client_cert_source is provided. - with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS": "auto"}): + # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT has + # unsupported value. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "Unsupported"}): + with pytest.raises(MutualTLSChannelError): + client = client_class() + + # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} + ): + with pytest.raises(ValueError): + client = client_class() + + # Check the case quota_project_id is provided + options = client_options.ClientOptions(quota_project_id="octopus") + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + ssl_channel_credentials=None, + quota_project_id="octopus", + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) + + +@pytest.mark.parametrize( + "client_class,transport_class,transport_name,use_client_cert_env", + [ + ( + TranslationServiceClient, + transports.TranslationServiceGrpcTransport, + "grpc", + "true", + ), + ( + TranslationServiceAsyncClient, + transports.TranslationServiceGrpcAsyncIOTransport, + "grpc_asyncio", + "true", + ), + ( + TranslationServiceClient, + transports.TranslationServiceGrpcTransport, + "grpc", + "false", + ), + ( + TranslationServiceAsyncClient, + transports.TranslationServiceGrpcAsyncIOTransport, + "grpc_asyncio", + "false", + ), + ], +) +@mock.patch.object( + TranslationServiceClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(TranslationServiceClient), +) +@mock.patch.object( + TranslationServiceAsyncClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(TranslationServiceAsyncClient), +) +@mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}) +def test_translation_service_client_mtls_env_auto( + client_class, transport_class, transport_name, use_client_cert_env +): + # This tests the endpoint autoswitch behavior. Endpoint is autoswitched to the default + # mtls endpoint, if GOOGLE_API_USE_CLIENT_CERTIFICATE is "true" and client cert exists. + + # Check the case client_cert_source is provided. Whether client cert is used depends on + # GOOGLE_API_USE_CLIENT_CERTIFICATE value. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): options = client_options.ClientOptions( client_cert_source=client_cert_source_callback ) with mock.patch.object(transport_class, "__init__") as patched: - patched.return_value = None - client = client_class(client_options=options) - patched.assert_called_once_with( - credentials=None, - credentials_file=None, - host=client.DEFAULT_MTLS_ENDPOINT, - scopes=None, - api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT, - client_cert_source=client_cert_source_callback, - quota_project_id=None, - ) - - # Check the case api_endpoint is not provided, GOOGLE_API_USE_MTLS is - # "auto", and default_client_cert_source is provided. - with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS": "auto"}): - with mock.patch.object(transport_class, "__init__") as patched: + ssl_channel_creds = mock.Mock() with mock.patch( - "google.auth.transport.mtls.has_default_client_cert_source", - return_value=True, + "grpc.ssl_channel_credentials", return_value=ssl_channel_creds ): patched.return_value = None - client = client_class() + client = client_class(client_options=options) + + if use_client_cert_env == "false": + expected_ssl_channel_creds = None + expected_host = client.DEFAULT_ENDPOINT + else: + expected_ssl_channel_creds = ssl_channel_creds + expected_host = client.DEFAULT_MTLS_ENDPOINT + patched.assert_called_once_with( credentials=None, credentials_file=None, - host=client.DEFAULT_MTLS_ENDPOINT, + host=expected_host, scopes=None, - api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT, - client_cert_source=None, + ssl_channel_credentials=expected_ssl_channel_creds, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) - # Check the case api_endpoint is not provided, GOOGLE_API_USE_MTLS is - # "auto", but client_cert_source and default_client_cert_source are None. - with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS": "auto"}): + # Check the case ADC client cert is provided. Whether client cert is used depends on + # GOOGLE_API_USE_CLIENT_CERTIFICATE value. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): with mock.patch.object(transport_class, "__init__") as patched: with mock.patch( - "google.auth.transport.mtls.has_default_client_cert_source", - return_value=False, + "google.auth.transport.grpc.SslCredentials.__init__", return_value=None ): - patched.return_value = None - client = client_class() - patched.assert_called_once_with( - credentials=None, - credentials_file=None, - host=client.DEFAULT_ENDPOINT, - scopes=None, - api_mtls_endpoint=client.DEFAULT_ENDPOINT, - client_cert_source=None, - quota_project_id=None, - ) - - # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS has - # unsupported value. - with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS": "Unsupported"}): - with pytest.raises(MutualTLSChannelError): - client = client_class() - - # Check the case quota_project_id is provided - options = client_options.ClientOptions(quota_project_id="octopus") - with mock.patch.object(transport_class, "__init__") as patched: - patched.return_value = None - client = client_class(client_options=options) - patched.assert_called_once_with( - credentials=None, - credentials_file=None, - host=client.DEFAULT_ENDPOINT, - scopes=None, - api_mtls_endpoint=client.DEFAULT_ENDPOINT, - client_cert_source=None, - quota_project_id="octopus", - ) + with mock.patch( + "google.auth.transport.grpc.SslCredentials.is_mtls", + new_callable=mock.PropertyMock, + ) as is_mtls_mock: + with mock.patch( + "google.auth.transport.grpc.SslCredentials.ssl_credentials", + new_callable=mock.PropertyMock, + ) as ssl_credentials_mock: + if use_client_cert_env == "false": + is_mtls_mock.return_value = False + ssl_credentials_mock.return_value = None + expected_host = client.DEFAULT_ENDPOINT + expected_ssl_channel_creds = None + else: + is_mtls_mock.return_value = True + ssl_credentials_mock.return_value = mock.Mock() + expected_host = client.DEFAULT_MTLS_ENDPOINT + expected_ssl_channel_creds = ( + ssl_credentials_mock.return_value + ) + + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=expected_host, + scopes=None, + ssl_channel_credentials=expected_ssl_channel_creds, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) + + # Check the case client_cert_source and ADC client cert are not provided. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): + with mock.patch.object(transport_class, "__init__") as patched: + with mock.patch( + "google.auth.transport.grpc.SslCredentials.__init__", return_value=None + ): + with mock.patch( + "google.auth.transport.grpc.SslCredentials.is_mtls", + new_callable=mock.PropertyMock, + ) as is_mtls_mock: + is_mtls_mock.return_value = False + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + ssl_channel_credentials=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) @pytest.mark.parametrize( @@ -307,9 +402,9 @@ def test_translation_service_client_client_options_scopes( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=["1", "2"], - api_mtls_endpoint=client.DEFAULT_ENDPOINT, - client_cert_source=None, + ssl_channel_credentials=None, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -337,9 +432,9 @@ def test_translation_service_client_client_options_credentials_file( credentials_file="credentials.json", host=client.DEFAULT_ENDPOINT, scopes=None, - api_mtls_endpoint=client.DEFAULT_ENDPOINT, - client_cert_source=None, + ssl_channel_credentials=None, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -356,9 +451,9 @@ def test_translation_service_client_client_options_from_dict(): credentials_file=None, host="squid.clam.whelk", scopes=None, - api_mtls_endpoint="squid.clam.whelk", - client_cert_source=None, + ssl_channel_credentials=None, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -1639,8 +1734,8 @@ def test_list_glossaries_pages(): RuntimeError, ) pages = list(client.list_glossaries(request={}).pages) - for page, token in zip(pages, ["abc", "def", "ghi", ""]): - assert page.raw_page.next_page_token == token + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token @pytest.mark.asyncio @@ -1726,10 +1821,10 @@ async def test_list_glossaries_async_pages(): RuntimeError, ) pages = [] - async for page in (await client.list_glossaries(request={})).pages: - pages.append(page) - for page, token in zip(pages, ["abc", "def", "ghi", ""]): - assert page.raw_page.next_page_token == token + async for page_ in (await client.list_glossaries(request={})).pages: + pages.append(page_) + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token def test_get_glossary( @@ -2182,6 +2277,21 @@ def test_transport_get_channel(): assert channel +@pytest.mark.parametrize( + "transport_class", + [ + transports.TranslationServiceGrpcTransport, + transports.TranslationServiceGrpcAsyncIOTransport, + ], +) +def test_transport_adc(transport_class): + # Test default credentials are used if not provided. + with mock.patch.object(auth, "default") as adc: + adc.return_value = (credentials.AnonymousCredentials(), None) + transport_class() + adc.assert_called_once() + + def test_transport_grpc_default(): # A client should use the gRPC transport by default. client = TranslationServiceClient(credentials=credentials.AnonymousCredentials(),) @@ -2251,6 +2361,17 @@ def test_translation_service_base_transport_with_credentials_file(): ) +def test_translation_service_base_transport_with_adc(): + # Test the default credentials are used if credentials and credentials_file are None. + with mock.patch.object(auth, "default") as adc, mock.patch( + "google.cloud.translate_v3.services.translation_service.transports.TranslationServiceTransport._prep_wrapped_messages" + ) as Transport: + Transport.return_value = None + adc.return_value = (credentials.AnonymousCredentials(), None) + transport = transports.TranslationServiceTransport() + adc.assert_called_once() + + def test_translation_service_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(auth, "default") as adc: @@ -2305,191 +2426,116 @@ def test_translation_service_host_with_port(): def test_translation_service_grpc_transport_channel(): channel = grpc.insecure_channel("http://localhost/") - # Check that if channel is provided, mtls endpoint and client_cert_source - # won't be used. - callback = mock.MagicMock() + # Check that channel is used if provided. transport = transports.TranslationServiceGrpcTransport( - host="squid.clam.whelk", - channel=channel, - api_mtls_endpoint="mtls.squid.clam.whelk", - client_cert_source=callback, + host="squid.clam.whelk", channel=channel, ) assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" - assert not callback.called def test_translation_service_grpc_asyncio_transport_channel(): channel = aio.insecure_channel("http://localhost/") - # Check that if channel is provided, mtls endpoint and client_cert_source - # won't be used. - callback = mock.MagicMock() + # Check that channel is used if provided. transport = transports.TranslationServiceGrpcAsyncIOTransport( - host="squid.clam.whelk", - channel=channel, - api_mtls_endpoint="mtls.squid.clam.whelk", - client_cert_source=callback, + host="squid.clam.whelk", channel=channel, ) assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" - assert not callback.called - - -@mock.patch("grpc.ssl_channel_credentials", autospec=True) -@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) -def test_translation_service_grpc_transport_channel_mtls_with_client_cert_source( - grpc_create_channel, grpc_ssl_channel_cred -): - # Check that if channel is None, but api_mtls_endpoint and client_cert_source - # are provided, then a mTLS channel will be created. - mock_cred = mock.Mock() - - mock_ssl_cred = mock.Mock() - grpc_ssl_channel_cred.return_value = mock_ssl_cred - - mock_grpc_channel = mock.Mock() - grpc_create_channel.return_value = mock_grpc_channel - - transport = transports.TranslationServiceGrpcTransport( - host="squid.clam.whelk", - credentials=mock_cred, - api_mtls_endpoint="mtls.squid.clam.whelk", - client_cert_source=client_cert_source_callback, - ) - grpc_ssl_channel_cred.assert_called_once_with( - certificate_chain=b"cert bytes", private_key=b"key bytes" - ) - grpc_create_channel.assert_called_once_with( - "mtls.squid.clam.whelk:443", - credentials=mock_cred, - credentials_file=None, - scopes=( - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-translation", - ), - ssl_credentials=mock_ssl_cred, - quota_project_id=None, - ) - assert transport.grpc_channel == mock_grpc_channel - - -@mock.patch("grpc.ssl_channel_credentials", autospec=True) -@mock.patch("google.api_core.grpc_helpers_async.create_channel", autospec=True) -def test_translation_service_grpc_asyncio_transport_channel_mtls_with_client_cert_source( - grpc_create_channel, grpc_ssl_channel_cred -): - # Check that if channel is None, but api_mtls_endpoint and client_cert_source - # are provided, then a mTLS channel will be created. - mock_cred = mock.Mock() - - mock_ssl_cred = mock.Mock() - grpc_ssl_channel_cred.return_value = mock_ssl_cred - - mock_grpc_channel = mock.Mock() - grpc_create_channel.return_value = mock_grpc_channel - - transport = transports.TranslationServiceGrpcAsyncIOTransport( - host="squid.clam.whelk", - credentials=mock_cred, - api_mtls_endpoint="mtls.squid.clam.whelk", - client_cert_source=client_cert_source_callback, - ) - grpc_ssl_channel_cred.assert_called_once_with( - certificate_chain=b"cert bytes", private_key=b"key bytes" - ) - grpc_create_channel.assert_called_once_with( - "mtls.squid.clam.whelk:443", - credentials=mock_cred, - credentials_file=None, - scopes=( - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-translation", - ), - ssl_credentials=mock_ssl_cred, - quota_project_id=None, - ) - assert transport.grpc_channel == mock_grpc_channel @pytest.mark.parametrize( - "api_mtls_endpoint", ["mtls.squid.clam.whelk", "mtls.squid.clam.whelk:443"] + "transport_class", + [ + transports.TranslationServiceGrpcTransport, + transports.TranslationServiceGrpcAsyncIOTransport, + ], ) -@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) -def test_translation_service_grpc_transport_channel_mtls_with_adc( - grpc_create_channel, api_mtls_endpoint +def test_translation_service_transport_channel_mtls_with_client_cert_source( + transport_class, ): - # Check that if channel and client_cert_source are None, but api_mtls_endpoint - # is provided, then a mTLS channel will be created with SSL ADC. - mock_grpc_channel = mock.Mock() - grpc_create_channel.return_value = mock_grpc_channel - - # Mock google.auth.transport.grpc.SslCredentials class. - mock_ssl_cred = mock.Mock() - with mock.patch.multiple( - "google.auth.transport.grpc.SslCredentials", - __init__=mock.Mock(return_value=None), - ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), - ): - mock_cred = mock.Mock() - transport = transports.TranslationServiceGrpcTransport( - host="squid.clam.whelk", - credentials=mock_cred, - api_mtls_endpoint=api_mtls_endpoint, - client_cert_source=None, - ) - grpc_create_channel.assert_called_once_with( - "mtls.squid.clam.whelk:443", - credentials=mock_cred, - credentials_file=None, - scopes=( - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-translation", - ), - ssl_credentials=mock_ssl_cred, - quota_project_id=None, - ) - assert transport.grpc_channel == mock_grpc_channel + with mock.patch( + "grpc.ssl_channel_credentials", autospec=True + ) as grpc_ssl_channel_cred: + with mock.patch.object( + transport_class, "create_channel", autospec=True + ) as grpc_create_channel: + mock_ssl_cred = mock.Mock() + grpc_ssl_channel_cred.return_value = mock_ssl_cred + + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + cred = credentials.AnonymousCredentials() + with pytest.warns(DeprecationWarning): + with mock.patch.object(auth, "default") as adc: + adc.return_value = (cred, None) + transport = transport_class( + host="squid.clam.whelk", + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=client_cert_source_callback, + ) + adc.assert_called_once() + + grpc_ssl_channel_cred.assert_called_once_with( + certificate_chain=b"cert bytes", private_key=b"key bytes" + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=cred, + credentials_file=None, + scopes=( + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-translation", + ), + ssl_credentials=mock_ssl_cred, + quota_project_id=None, + ) + assert transport.grpc_channel == mock_grpc_channel @pytest.mark.parametrize( - "api_mtls_endpoint", ["mtls.squid.clam.whelk", "mtls.squid.clam.whelk:443"] + "transport_class", + [ + transports.TranslationServiceGrpcTransport, + transports.TranslationServiceGrpcAsyncIOTransport, + ], ) -@mock.patch("google.api_core.grpc_helpers_async.create_channel", autospec=True) -def test_translation_service_grpc_asyncio_transport_channel_mtls_with_adc( - grpc_create_channel, api_mtls_endpoint -): - # Check that if channel and client_cert_source are None, but api_mtls_endpoint - # is provided, then a mTLS channel will be created with SSL ADC. - mock_grpc_channel = mock.Mock() - grpc_create_channel.return_value = mock_grpc_channel - - # Mock google.auth.transport.grpc.SslCredentials class. +def test_translation_service_transport_channel_mtls_with_adc(transport_class): mock_ssl_cred = mock.Mock() with mock.patch.multiple( "google.auth.transport.grpc.SslCredentials", __init__=mock.Mock(return_value=None), ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), ): - mock_cred = mock.Mock() - transport = transports.TranslationServiceGrpcAsyncIOTransport( - host="squid.clam.whelk", - credentials=mock_cred, - api_mtls_endpoint=api_mtls_endpoint, - client_cert_source=None, - ) - grpc_create_channel.assert_called_once_with( - "mtls.squid.clam.whelk:443", - credentials=mock_cred, - credentials_file=None, - scopes=( - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-translation", - ), - ssl_credentials=mock_ssl_cred, - quota_project_id=None, - ) - assert transport.grpc_channel == mock_grpc_channel + with mock.patch.object( + transport_class, "create_channel", autospec=True + ) as grpc_create_channel: + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + mock_cred = mock.Mock() + + with pytest.warns(DeprecationWarning): + transport = transport_class( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=None, + ) + + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + credentials_file=None, + scopes=( + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-translation", + ), + ssl_credentials=mock_ssl_cred, + quota_project_id=None, + ) + assert transport.grpc_channel == mock_grpc_channel def test_translation_service_grpc_lro_client(): @@ -2541,3 +2587,24 @@ def test_parse_glossary_path(): # Check that the path construction is reversible. actual = TranslationServiceClient.parse_glossary_path(path) assert expected == actual + + +def test_client_withDEFAULT_CLIENT_INFO(): + client_info = gapic_v1.client_info.ClientInfo() + + with mock.patch.object( + transports.TranslationServiceTransport, "_prep_wrapped_messages" + ) as prep: + client = TranslationServiceClient( + credentials=credentials.AnonymousCredentials(), client_info=client_info, + ) + prep.assert_called_once_with(client_info) + + with mock.patch.object( + transports.TranslationServiceTransport, "_prep_wrapped_messages" + ) as prep: + transport_class = TranslationServiceClient.get_transport_class() + transport = transport_class( + credentials=credentials.AnonymousCredentials(), client_info=client_info, + ) + prep.assert_called_once_with(client_info) diff --git a/tests/unit/gapic/translate_v3beta1/test_translation_service.py b/tests/unit/gapic/translate_v3beta1/test_translation_service.py index 1dd49ee1..5af7b5c2 100644 --- a/tests/unit/gapic/translate_v3beta1/test_translation_service.py +++ b/tests/unit/gapic/translate_v3beta1/test_translation_service.py @@ -31,7 +31,7 @@ from google.api_core import gapic_v1 from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async -from google.api_core import operation_async +from google.api_core import operation_async # type: ignore from google.api_core import operations_v1 from google.auth import credentials from google.auth.exceptions import MutualTLSChannelError @@ -165,14 +165,14 @@ def test_translation_service_client_client_options( credentials_file=None, host="squid.clam.whelk", scopes=None, - api_mtls_endpoint="squid.clam.whelk", - client_cert_source=None, + ssl_channel_credentials=None, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) - # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS is + # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT is # "never". - with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS": "never"}): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None client = client_class() @@ -181,14 +181,14 @@ def test_translation_service_client_client_options( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=None, - api_mtls_endpoint=client.DEFAULT_ENDPOINT, - client_cert_source=None, + ssl_channel_credentials=None, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) - # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS is + # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT is # "always". - with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS": "always"}): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None client = client_class() @@ -197,90 +197,185 @@ def test_translation_service_client_client_options( credentials_file=None, host=client.DEFAULT_MTLS_ENDPOINT, scopes=None, - api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT, - client_cert_source=None, + ssl_channel_credentials=None, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) - # Check the case api_endpoint is not provided, GOOGLE_API_USE_MTLS is - # "auto", and client_cert_source is provided. - with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS": "auto"}): + # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT has + # unsupported value. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "Unsupported"}): + with pytest.raises(MutualTLSChannelError): + client = client_class() + + # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} + ): + with pytest.raises(ValueError): + client = client_class() + + # Check the case quota_project_id is provided + options = client_options.ClientOptions(quota_project_id="octopus") + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + ssl_channel_credentials=None, + quota_project_id="octopus", + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) + + +@pytest.mark.parametrize( + "client_class,transport_class,transport_name,use_client_cert_env", + [ + ( + TranslationServiceClient, + transports.TranslationServiceGrpcTransport, + "grpc", + "true", + ), + ( + TranslationServiceAsyncClient, + transports.TranslationServiceGrpcAsyncIOTransport, + "grpc_asyncio", + "true", + ), + ( + TranslationServiceClient, + transports.TranslationServiceGrpcTransport, + "grpc", + "false", + ), + ( + TranslationServiceAsyncClient, + transports.TranslationServiceGrpcAsyncIOTransport, + "grpc_asyncio", + "false", + ), + ], +) +@mock.patch.object( + TranslationServiceClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(TranslationServiceClient), +) +@mock.patch.object( + TranslationServiceAsyncClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(TranslationServiceAsyncClient), +) +@mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}) +def test_translation_service_client_mtls_env_auto( + client_class, transport_class, transport_name, use_client_cert_env +): + # This tests the endpoint autoswitch behavior. Endpoint is autoswitched to the default + # mtls endpoint, if GOOGLE_API_USE_CLIENT_CERTIFICATE is "true" and client cert exists. + + # Check the case client_cert_source is provided. Whether client cert is used depends on + # GOOGLE_API_USE_CLIENT_CERTIFICATE value. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): options = client_options.ClientOptions( client_cert_source=client_cert_source_callback ) with mock.patch.object(transport_class, "__init__") as patched: - patched.return_value = None - client = client_class(client_options=options) - patched.assert_called_once_with( - credentials=None, - credentials_file=None, - host=client.DEFAULT_MTLS_ENDPOINT, - scopes=None, - api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT, - client_cert_source=client_cert_source_callback, - quota_project_id=None, - ) - - # Check the case api_endpoint is not provided, GOOGLE_API_USE_MTLS is - # "auto", and default_client_cert_source is provided. - with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS": "auto"}): - with mock.patch.object(transport_class, "__init__") as patched: + ssl_channel_creds = mock.Mock() with mock.patch( - "google.auth.transport.mtls.has_default_client_cert_source", - return_value=True, + "grpc.ssl_channel_credentials", return_value=ssl_channel_creds ): patched.return_value = None - client = client_class() + client = client_class(client_options=options) + + if use_client_cert_env == "false": + expected_ssl_channel_creds = None + expected_host = client.DEFAULT_ENDPOINT + else: + expected_ssl_channel_creds = ssl_channel_creds + expected_host = client.DEFAULT_MTLS_ENDPOINT + patched.assert_called_once_with( credentials=None, credentials_file=None, - host=client.DEFAULT_MTLS_ENDPOINT, + host=expected_host, scopes=None, - api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT, - client_cert_source=None, + ssl_channel_credentials=expected_ssl_channel_creds, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) - # Check the case api_endpoint is not provided, GOOGLE_API_USE_MTLS is - # "auto", but client_cert_source and default_client_cert_source are None. - with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS": "auto"}): + # Check the case ADC client cert is provided. Whether client cert is used depends on + # GOOGLE_API_USE_CLIENT_CERTIFICATE value. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): with mock.patch.object(transport_class, "__init__") as patched: with mock.patch( - "google.auth.transport.mtls.has_default_client_cert_source", - return_value=False, + "google.auth.transport.grpc.SslCredentials.__init__", return_value=None ): - patched.return_value = None - client = client_class() - patched.assert_called_once_with( - credentials=None, - credentials_file=None, - host=client.DEFAULT_ENDPOINT, - scopes=None, - api_mtls_endpoint=client.DEFAULT_ENDPOINT, - client_cert_source=None, - quota_project_id=None, - ) - - # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS has - # unsupported value. - with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS": "Unsupported"}): - with pytest.raises(MutualTLSChannelError): - client = client_class() - - # Check the case quota_project_id is provided - options = client_options.ClientOptions(quota_project_id="octopus") - with mock.patch.object(transport_class, "__init__") as patched: - patched.return_value = None - client = client_class(client_options=options) - patched.assert_called_once_with( - credentials=None, - credentials_file=None, - host=client.DEFAULT_ENDPOINT, - scopes=None, - api_mtls_endpoint=client.DEFAULT_ENDPOINT, - client_cert_source=None, - quota_project_id="octopus", - ) + with mock.patch( + "google.auth.transport.grpc.SslCredentials.is_mtls", + new_callable=mock.PropertyMock, + ) as is_mtls_mock: + with mock.patch( + "google.auth.transport.grpc.SslCredentials.ssl_credentials", + new_callable=mock.PropertyMock, + ) as ssl_credentials_mock: + if use_client_cert_env == "false": + is_mtls_mock.return_value = False + ssl_credentials_mock.return_value = None + expected_host = client.DEFAULT_ENDPOINT + expected_ssl_channel_creds = None + else: + is_mtls_mock.return_value = True + ssl_credentials_mock.return_value = mock.Mock() + expected_host = client.DEFAULT_MTLS_ENDPOINT + expected_ssl_channel_creds = ( + ssl_credentials_mock.return_value + ) + + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=expected_host, + scopes=None, + ssl_channel_credentials=expected_ssl_channel_creds, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) + + # Check the case client_cert_source and ADC client cert are not provided. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): + with mock.patch.object(transport_class, "__init__") as patched: + with mock.patch( + "google.auth.transport.grpc.SslCredentials.__init__", return_value=None + ): + with mock.patch( + "google.auth.transport.grpc.SslCredentials.is_mtls", + new_callable=mock.PropertyMock, + ) as is_mtls_mock: + is_mtls_mock.return_value = False + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + ssl_channel_credentials=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) @pytest.mark.parametrize( @@ -307,9 +402,9 @@ def test_translation_service_client_client_options_scopes( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=["1", "2"], - api_mtls_endpoint=client.DEFAULT_ENDPOINT, - client_cert_source=None, + ssl_channel_credentials=None, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -337,9 +432,9 @@ def test_translation_service_client_client_options_credentials_file( credentials_file="credentials.json", host=client.DEFAULT_ENDPOINT, scopes=None, - api_mtls_endpoint=client.DEFAULT_ENDPOINT, - client_cert_source=None, + ssl_channel_credentials=None, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -356,9 +451,9 @@ def test_translation_service_client_client_options_from_dict(): credentials_file=None, host="squid.clam.whelk", scopes=None, - api_mtls_endpoint="squid.clam.whelk", - client_cert_source=None, + ssl_channel_credentials=None, quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -1520,8 +1615,8 @@ def test_list_glossaries_pages(): RuntimeError, ) pages = list(client.list_glossaries(request={}).pages) - for page, token in zip(pages, ["abc", "def", "ghi", ""]): - assert page.raw_page.next_page_token == token + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token @pytest.mark.asyncio @@ -1607,10 +1702,10 @@ async def test_list_glossaries_async_pages(): RuntimeError, ) pages = [] - async for page in (await client.list_glossaries(request={})).pages: - pages.append(page) - for page, token in zip(pages, ["abc", "def", "ghi", ""]): - assert page.raw_page.next_page_token == token + async for page_ in (await client.list_glossaries(request={})).pages: + pages.append(page_) + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token def test_get_glossary( @@ -2063,6 +2158,21 @@ def test_transport_get_channel(): assert channel +@pytest.mark.parametrize( + "transport_class", + [ + transports.TranslationServiceGrpcTransport, + transports.TranslationServiceGrpcAsyncIOTransport, + ], +) +def test_transport_adc(transport_class): + # Test default credentials are used if not provided. + with mock.patch.object(auth, "default") as adc: + adc.return_value = (credentials.AnonymousCredentials(), None) + transport_class() + adc.assert_called_once() + + def test_transport_grpc_default(): # A client should use the gRPC transport by default. client = TranslationServiceClient(credentials=credentials.AnonymousCredentials(),) @@ -2132,6 +2242,17 @@ def test_translation_service_base_transport_with_credentials_file(): ) +def test_translation_service_base_transport_with_adc(): + # Test the default credentials are used if credentials and credentials_file are None. + with mock.patch.object(auth, "default") as adc, mock.patch( + "google.cloud.translate_v3beta1.services.translation_service.transports.TranslationServiceTransport._prep_wrapped_messages" + ) as Transport: + Transport.return_value = None + adc.return_value = (credentials.AnonymousCredentials(), None) + transport = transports.TranslationServiceTransport() + adc.assert_called_once() + + def test_translation_service_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(auth, "default") as adc: @@ -2186,191 +2307,116 @@ def test_translation_service_host_with_port(): def test_translation_service_grpc_transport_channel(): channel = grpc.insecure_channel("http://localhost/") - # Check that if channel is provided, mtls endpoint and client_cert_source - # won't be used. - callback = mock.MagicMock() + # Check that channel is used if provided. transport = transports.TranslationServiceGrpcTransport( - host="squid.clam.whelk", - channel=channel, - api_mtls_endpoint="mtls.squid.clam.whelk", - client_cert_source=callback, + host="squid.clam.whelk", channel=channel, ) assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" - assert not callback.called def test_translation_service_grpc_asyncio_transport_channel(): channel = aio.insecure_channel("http://localhost/") - # Check that if channel is provided, mtls endpoint and client_cert_source - # won't be used. - callback = mock.MagicMock() + # Check that channel is used if provided. transport = transports.TranslationServiceGrpcAsyncIOTransport( - host="squid.clam.whelk", - channel=channel, - api_mtls_endpoint="mtls.squid.clam.whelk", - client_cert_source=callback, + host="squid.clam.whelk", channel=channel, ) assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" - assert not callback.called - - -@mock.patch("grpc.ssl_channel_credentials", autospec=True) -@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) -def test_translation_service_grpc_transport_channel_mtls_with_client_cert_source( - grpc_create_channel, grpc_ssl_channel_cred -): - # Check that if channel is None, but api_mtls_endpoint and client_cert_source - # are provided, then a mTLS channel will be created. - mock_cred = mock.Mock() - - mock_ssl_cred = mock.Mock() - grpc_ssl_channel_cred.return_value = mock_ssl_cred - - mock_grpc_channel = mock.Mock() - grpc_create_channel.return_value = mock_grpc_channel - - transport = transports.TranslationServiceGrpcTransport( - host="squid.clam.whelk", - credentials=mock_cred, - api_mtls_endpoint="mtls.squid.clam.whelk", - client_cert_source=client_cert_source_callback, - ) - grpc_ssl_channel_cred.assert_called_once_with( - certificate_chain=b"cert bytes", private_key=b"key bytes" - ) - grpc_create_channel.assert_called_once_with( - "mtls.squid.clam.whelk:443", - credentials=mock_cred, - credentials_file=None, - scopes=( - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-translation", - ), - ssl_credentials=mock_ssl_cred, - quota_project_id=None, - ) - assert transport.grpc_channel == mock_grpc_channel - - -@mock.patch("grpc.ssl_channel_credentials", autospec=True) -@mock.patch("google.api_core.grpc_helpers_async.create_channel", autospec=True) -def test_translation_service_grpc_asyncio_transport_channel_mtls_with_client_cert_source( - grpc_create_channel, grpc_ssl_channel_cred -): - # Check that if channel is None, but api_mtls_endpoint and client_cert_source - # are provided, then a mTLS channel will be created. - mock_cred = mock.Mock() - - mock_ssl_cred = mock.Mock() - grpc_ssl_channel_cred.return_value = mock_ssl_cred - - mock_grpc_channel = mock.Mock() - grpc_create_channel.return_value = mock_grpc_channel - - transport = transports.TranslationServiceGrpcAsyncIOTransport( - host="squid.clam.whelk", - credentials=mock_cred, - api_mtls_endpoint="mtls.squid.clam.whelk", - client_cert_source=client_cert_source_callback, - ) - grpc_ssl_channel_cred.assert_called_once_with( - certificate_chain=b"cert bytes", private_key=b"key bytes" - ) - grpc_create_channel.assert_called_once_with( - "mtls.squid.clam.whelk:443", - credentials=mock_cred, - credentials_file=None, - scopes=( - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-translation", - ), - ssl_credentials=mock_ssl_cred, - quota_project_id=None, - ) - assert transport.grpc_channel == mock_grpc_channel @pytest.mark.parametrize( - "api_mtls_endpoint", ["mtls.squid.clam.whelk", "mtls.squid.clam.whelk:443"] + "transport_class", + [ + transports.TranslationServiceGrpcTransport, + transports.TranslationServiceGrpcAsyncIOTransport, + ], ) -@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) -def test_translation_service_grpc_transport_channel_mtls_with_adc( - grpc_create_channel, api_mtls_endpoint +def test_translation_service_transport_channel_mtls_with_client_cert_source( + transport_class, ): - # Check that if channel and client_cert_source are None, but api_mtls_endpoint - # is provided, then a mTLS channel will be created with SSL ADC. - mock_grpc_channel = mock.Mock() - grpc_create_channel.return_value = mock_grpc_channel - - # Mock google.auth.transport.grpc.SslCredentials class. - mock_ssl_cred = mock.Mock() - with mock.patch.multiple( - "google.auth.transport.grpc.SslCredentials", - __init__=mock.Mock(return_value=None), - ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), - ): - mock_cred = mock.Mock() - transport = transports.TranslationServiceGrpcTransport( - host="squid.clam.whelk", - credentials=mock_cred, - api_mtls_endpoint=api_mtls_endpoint, - client_cert_source=None, - ) - grpc_create_channel.assert_called_once_with( - "mtls.squid.clam.whelk:443", - credentials=mock_cred, - credentials_file=None, - scopes=( - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-translation", - ), - ssl_credentials=mock_ssl_cred, - quota_project_id=None, - ) - assert transport.grpc_channel == mock_grpc_channel + with mock.patch( + "grpc.ssl_channel_credentials", autospec=True + ) as grpc_ssl_channel_cred: + with mock.patch.object( + transport_class, "create_channel", autospec=True + ) as grpc_create_channel: + mock_ssl_cred = mock.Mock() + grpc_ssl_channel_cred.return_value = mock_ssl_cred + + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + cred = credentials.AnonymousCredentials() + with pytest.warns(DeprecationWarning): + with mock.patch.object(auth, "default") as adc: + adc.return_value = (cred, None) + transport = transport_class( + host="squid.clam.whelk", + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=client_cert_source_callback, + ) + adc.assert_called_once() + + grpc_ssl_channel_cred.assert_called_once_with( + certificate_chain=b"cert bytes", private_key=b"key bytes" + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=cred, + credentials_file=None, + scopes=( + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-translation", + ), + ssl_credentials=mock_ssl_cred, + quota_project_id=None, + ) + assert transport.grpc_channel == mock_grpc_channel @pytest.mark.parametrize( - "api_mtls_endpoint", ["mtls.squid.clam.whelk", "mtls.squid.clam.whelk:443"] + "transport_class", + [ + transports.TranslationServiceGrpcTransport, + transports.TranslationServiceGrpcAsyncIOTransport, + ], ) -@mock.patch("google.api_core.grpc_helpers_async.create_channel", autospec=True) -def test_translation_service_grpc_asyncio_transport_channel_mtls_with_adc( - grpc_create_channel, api_mtls_endpoint -): - # Check that if channel and client_cert_source are None, but api_mtls_endpoint - # is provided, then a mTLS channel will be created with SSL ADC. - mock_grpc_channel = mock.Mock() - grpc_create_channel.return_value = mock_grpc_channel - - # Mock google.auth.transport.grpc.SslCredentials class. +def test_translation_service_transport_channel_mtls_with_adc(transport_class): mock_ssl_cred = mock.Mock() with mock.patch.multiple( "google.auth.transport.grpc.SslCredentials", __init__=mock.Mock(return_value=None), ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), ): - mock_cred = mock.Mock() - transport = transports.TranslationServiceGrpcAsyncIOTransport( - host="squid.clam.whelk", - credentials=mock_cred, - api_mtls_endpoint=api_mtls_endpoint, - client_cert_source=None, - ) - grpc_create_channel.assert_called_once_with( - "mtls.squid.clam.whelk:443", - credentials=mock_cred, - credentials_file=None, - scopes=( - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-translation", - ), - ssl_credentials=mock_ssl_cred, - quota_project_id=None, - ) - assert transport.grpc_channel == mock_grpc_channel + with mock.patch.object( + transport_class, "create_channel", autospec=True + ) as grpc_create_channel: + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + mock_cred = mock.Mock() + + with pytest.warns(DeprecationWarning): + transport = transport_class( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=None, + ) + + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + credentials_file=None, + scopes=( + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-translation", + ), + ssl_credentials=mock_ssl_cred, + quota_project_id=None, + ) + assert transport.grpc_channel == mock_grpc_channel def test_translation_service_grpc_lro_client(): @@ -2422,3 +2468,24 @@ def test_parse_glossary_path(): # Check that the path construction is reversible. actual = TranslationServiceClient.parse_glossary_path(path) assert expected == actual + + +def test_client_withDEFAULT_CLIENT_INFO(): + client_info = gapic_v1.client_info.ClientInfo() + + with mock.patch.object( + transports.TranslationServiceTransport, "_prep_wrapped_messages" + ) as prep: + client = TranslationServiceClient( + credentials=credentials.AnonymousCredentials(), client_info=client_info, + ) + prep.assert_called_once_with(client_info) + + with mock.patch.object( + transports.TranslationServiceTransport, "_prep_wrapped_messages" + ) as prep: + transport_class = TranslationServiceClient.get_transport_class() + transport = transport_class( + credentials=credentials.AnonymousCredentials(), client_info=client_info, + ) + prep.assert_called_once_with(client_info) From 86b7fa13e6f3ecaff5f0d346b6bad415b0090fd1 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 28 Oct 2020 19:18:10 +0100 Subject: [PATCH 46/50] chore(deps): update dependency google-cloud-automl to v2.1.0 (#77) --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 5896f2e2..1c85e6c6 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,3 @@ google-cloud-translate==3.0.1 google-cloud-storage==1.32.0 -google-cloud-automl==2.0.0 +google-cloud-automl==2.1.0 From 6fb2effa6903cae5584f51a74d1399f12697db1f Mon Sep 17 00:00:00 2001 From: Mike <45373284+munkhuushmgl@users.noreply.github.com> Date: Thu, 29 Oct 2020 12:32:58 -0700 Subject: [PATCH 47/50] chore: fix flaky tests (#78) * chore: fix flaky tests * fixed the lint * removed yaml file --- samples/snippets/beta_snippets.py | 2 +- .../translate_v3_batch_translate_text_test.py | 2 +- ...e_v3_batch_translate_text_with_glossary.py | 2 +- ..._translate_text_with_glossary_and_model.py | 81 +++---------------- ...slate_text_with_glossary_and_model_test.py | 5 +- ...batch_translate_text_with_glossary_test.py | 2 +- 6 files changed, 17 insertions(+), 77 deletions(-) diff --git a/samples/snippets/beta_snippets.py b/samples/snippets/beta_snippets.py index 835e55b1..6b770e61 100644 --- a/samples/snippets/beta_snippets.py +++ b/samples/snippets/beta_snippets.py @@ -77,7 +77,7 @@ def batch_translate_text(project_id, input_uri, output_uri): } ) - result = operation.result(timeout=240) + result = operation.result(timeout=320) print("Total Characters: {}".format(result.total_characters)) print("Translated Characters: {}".format(result.translated_characters)) diff --git a/samples/snippets/translate_v3_batch_translate_text_test.py b/samples/snippets/translate_v3_batch_translate_text_test.py index 8629d475..ad637cd8 100644 --- a/samples/snippets/translate_v3_batch_translate_text_test.py +++ b/samples/snippets/translate_v3_batch_translate_text_test.py @@ -42,7 +42,7 @@ def test_batch_translate_text(capsys, bucket): "gs://cloud-samples-data/translation/text.txt", "gs://{}/translation/BATCH_TRANSLATION_OUTPUT/".format(bucket.name), PROJECT_ID, - timeout=240, + timeout=320, ) out, _ = capsys.readouterr() assert "Total Characters" in out diff --git a/samples/snippets/translate_v3_batch_translate_text_with_glossary.py b/samples/snippets/translate_v3_batch_translate_text_with_glossary.py index 62500342..574b001c 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_glossary.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_glossary.py @@ -22,7 +22,7 @@ def batch_translate_text_with_glossary( output_uri="gs://YOUR_BUCKET_ID/path/to/save/results/", project_id="YOUR_PROJECT_ID", glossary_id="YOUR_GLOSSARY_ID", - timeout=180, + timeout=320, ): """Translates a batch of texts on GCS and stores the result in a GCS location. Glossary is applied for translation.""" diff --git a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py index e9a6905e..3110dcee 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model.py @@ -1,12 +1,10 @@ -# -*- 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 +# http://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, @@ -14,29 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -# DO NOT EDIT! This is a generated sample ("LongRunningPromise", "translate_v3_batch_translate_text_with_glossary_and_model") - -# To install the latest published package dependency, execute the following: -# pip install google-cloud-translate - -# sample-metadata -# title: Batch Translate with Glossary and Model -# description: Batch translate text with Glossary using AutoML Translation model -# usage: python3 translate_v3_batch_translate_text_with_glossary_and_model.py [--input_uri "gs://cloud-samples-data/text.txt"] [--output_uri "gs://YOUR_BUCKET_ID/path_to_store_results/"] [--project "[Google Cloud Project ID]"] [--location "us-central1"] [--target_language en] [--source_language de] [--model_id "{your-model-id}"] [--glossary_id "{your-glossary-id}"] - # [START translate_v3_batch_translate_text_with_glossary_and_model] from google.cloud import translate -def sample_batch_translate_text_with_glossary_and_model( - input_uri, - output_uri, - project_id, - location, - target_language, - source_language, - model_id, - glossary_id, +def batch_translate_text_with_glossary_and_model( + input_uri="gs://YOUR_BUCKET_ID/path/to/your/file.txt", + output_uri="gs://YOUR_BUCKET_ID/path/to/save/results/", + project_id="YOUR_PROJECT_ID", + model_id="YOUR_MODEL_ID", + glossary_id="YOUR_GLOSSARY_ID", ): """ Batch translate text with Glossary and Translation model @@ -44,16 +29,10 @@ def sample_batch_translate_text_with_glossary_and_model( client = translate.TranslationServiceClient() - # TODO(developer): Uncomment and set the following variables - # input_uri = 'gs://cloud-samples-data/text.txt' - # output_uri = 'gs://YOUR_BUCKET_ID/path_to_store_results/' - # project = '[Google Cloud Project ID]' - # location = 'us-central1' - # target_language = 'en' - # source_language = 'de' - # model_id = '{your-model-id}' - # glossary_id = '[YOUR_GLOSSARY_ID]' - target_language_codes = [target_language] + # Supported language codes: https://cloud.google.com/translate/docs/languages + location = "us-central1" + + target_language_codes = ["ja"] gcs_source = {"input_uri": input_uri} # Optional. Can be "text/plain" or "text/html". @@ -66,7 +45,7 @@ def sample_batch_translate_text_with_glossary_and_model( model_path = "projects/{}/locations/{}/models/{}".format( project_id, "us-central1", model_id ) - models = {target_language: model_path} + models = {"ja": model_path} glossary_path = client.glossary_path( project_id, "us-central1", glossary_id # The location of the glossary @@ -96,39 +75,3 @@ def sample_batch_translate_text_with_glossary_and_model( # [END translate_v3_batch_translate_text_with_glossary_and_model] - - -def main(): - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument( - "--input_uri", type=str, default="gs://cloud-samples-data/text.txt" - ) - parser.add_argument( - "--output_uri", type=str, default="gs://YOUR_BUCKET_ID/path_to_store_results/" - ) - parser.add_argument("--project_id", type=str, default="[Google Cloud Project ID]") - parser.add_argument("--location", type=str, default="us-central1") - parser.add_argument("--target_language", type=str, default="en") - parser.add_argument("--source_language", type=str, default="de") - parser.add_argument("--model_id", type=str, default="{your-model-id}") - parser.add_argument( - "--glossary_id", type=str, default="[YOUR_GLOSSARY_ID]", - ) - args = parser.parse_args() - - sample_batch_translate_text_with_glossary_and_model( - args.input_uri, - args.output_uri, - args.project_id, - args.location, - args.target_language, - args.source_language, - args.model_id, - args.glossary_id, - ) - - -if __name__ == "__main__": - main() diff --git a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py index 6ba15118..6579831a 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_glossary_and_model_test.py @@ -56,13 +56,10 @@ def bucket(): def test_batch_translate_text_with_glossary_and_model(capsys, bucket, glossary): - translate_v3_batch_translate_text_with_glossary_and_model.sample_batch_translate_text_with_glossary_and_model( + translate_v3_batch_translate_text_with_glossary_and_model.batch_translate_text_with_glossary_and_model( "gs://cloud-samples-data/translation/text_with_custom_model_and_glossary.txt", "gs://{}/translation/BATCH_TRANSLATION_OUTPUT/".format(bucket.name), PROJECT_ID, - "us-central1", - "ja", - "en", MODEL_ID, glossary, ) diff --git a/samples/snippets/translate_v3_batch_translate_text_with_glossary_test.py b/samples/snippets/translate_v3_batch_translate_text_with_glossary_test.py index 726a8e0c..33a1f829 100644 --- a/samples/snippets/translate_v3_batch_translate_text_with_glossary_test.py +++ b/samples/snippets/translate_v3_batch_translate_text_with_glossary_test.py @@ -73,7 +73,7 @@ def test_batch_translate_text_with_glossary(capsys, bucket, glossary): "gs://{}/translation/BATCH_TRANSLATION_OUTPUT/".format(bucket.name), PROJECT_ID, glossary, - 240, + 320, ) out, _ = capsys.readouterr() From ed89ea135e7c9da6aec9e13d3ab38ad408964328 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 25 Nov 2020 20:32:50 +0100 Subject: [PATCH 48/50] chore(deps): update dependency google-cloud-storage to v1.33.0 (#79) --- samples/snippets/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 1c85e6c6..4f26184f 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,3 +1,3 @@ google-cloud-translate==3.0.1 -google-cloud-storage==1.32.0 +google-cloud-storage==1.33.0 google-cloud-automl==2.1.0 From 1399cf0affcff48ebc078503822cdcb49a1e4b14 Mon Sep 17 00:00:00 2001 From: Eric Schmidt Date: Wed, 9 Dec 2020 09:36:39 -0800 Subject: [PATCH 49/50] fix: moves import six inside of region tags (#83) --- samples/snippets/snippets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/snippets/snippets.py b/samples/snippets/snippets.py index 41d64c32..5700969c 100644 --- a/samples/snippets/snippets.py +++ b/samples/snippets/snippets.py @@ -23,8 +23,6 @@ import argparse -import six - def detect_language(text): # [START translate_detect_language] @@ -84,6 +82,7 @@ def translate_text_with_model(target, text, model="nmt"): Target must be an ISO 639-1 language code. See https://g.co/cloud/translate/v2/translate-reference#supported_languages """ + import six from google.cloud import translate_v2 as translate translate_client = translate.Client() @@ -108,6 +107,7 @@ def translate_text(target, text): Target must be an ISO 639-1 language code. See https://g.co/cloud/translate/v2/translate-reference#supported_languages """ + import six from google.cloud import translate_v2 as translate translate_client = translate.Client() From 44a00eb45a440338cbcd262c3968772b26aa3316 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 11 Dec 2020 00:38:03 +0000 Subject: [PATCH 50/50] chore: release 3.0.2 (#62) :robot: I have created a release \*beep\* \*boop\* --- ### [3.0.2](https://www.github.com/googleapis/python-translate/compare/v3.0.1...v3.0.2) (2020-12-09) ### Documentation * add w/ glossary and model ([1e030d4](https://www.github.com/googleapis/python-translate/commit/1e030d4557ee1f67bad5e5b4759d0200efd27afd)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11aa7cfa..e0644800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-cloud-translate/#history +### [3.0.2](https://www.github.com/googleapis/python-translate/compare/v3.0.1...v3.0.2) (2020-12-09) + + +### Documentation + +* add w/ glossary and model ([1e030d4](https://www.github.com/googleapis/python-translate/commit/1e030d4557ee1f67bad5e5b4759d0200efd27afd)) + ### [3.0.1](https://www.github.com/googleapis/python-translate/compare/v3.0.0...v3.0.1) (2020-08-08) diff --git a/setup.py b/setup.py index 047a6707..9045fdcf 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ name = "google-cloud-translate" description = "Google Cloud Translation API client library" -version = "3.0.1" +version = "3.0.2" # Should be one of: # 'Development Status :: 3 - Alpha' # 'Development Status :: 4 - Beta'