From 92f3cc17d4e6244363939f1983710dc31b0dc574 Mon Sep 17 00:00:00 2001 From: aribray <45905583+aribray@users.noreply.github.com> Date: Mon, 19 Oct 2020 11:56:11 -0700 Subject: [PATCH 01/30] Chore: Add requirements.txt and noxfile.py for new samples (#45) * add noxfile and requirements.txt * refactor requirements.txt * add newline * add newline * add newline --- samples/snippets/noxfile.py | 236 +++++++++++++++++++++++++ samples/snippets/requirements-test.txt | 1 + samples/snippets/requirements.txt | 2 + 3 files changed, 239 insertions(+) create mode 100644 samples/snippets/noxfile.py create mode 100644 samples/snippets/requirements-test.txt create mode 100644 samples/snippets/requirements.txt diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py new file mode 100644 index 00000000..eb2b6b9e --- /dev/null +++ b/samples/snippets/noxfile.py @@ -0,0 +1,236 @@ +# 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. + +from __future__ import print_function + +import os +from pathlib import Path +import sys + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +# Copy `noxfile_config.py` to your directory and modify it instead. + + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + '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': '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': {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append('.') + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars(): + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG['gcloud_project_env'] + # This should error out if not set. + ret['GOOGLE_CLOUD_PROJECT'] = os.environ[env_key] + ret['GCLOUD_PROJECT'] = os.environ[env_key] # deprecated + + # Apply user supplied envs. + ret.update(TEST_CONFIG['envs']) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to tested samples. +ALL_VERSIONS = ["2.7", "3.6", "3.7", "3.8"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG['ignored_versions'] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = bool(os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False)) +# +# Style Checks +# + + +def _determine_local_import_names(start_dir): + """Determines all import names that should be considered "local". + This is used when running the linter to insure that import order is + properly checked. + """ + file_ext_pairs = [os.path.splitext(path) for path in os.listdir(start_dir)] + return [ + basename + for basename, extension in file_ext_pairs + if extension == ".py" + or os.path.isdir(os.path.join(start_dir, basename)) + and basename not in ("__pycache__") + ] + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--import-order-style=google", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session): + session.install("flake8", "flake8-import-order") + + local_names = _determine_local_import_names(".") + args = FLAKE8_COMMON_ARGS + [ + "--application-import-names", + ",".join(local_names), + "." + ] + session.run("flake8", *args) + + +# +# Black +# + +@nox.session +def blacken(session): + session.install("black") + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests(session, post_install=None): + """Runs py.test for a particular project.""" + if os.path.exists("requirements.txt"): + session.install("-r", "requirements.txt") + + if os.path.exists("requirements-test.txt"): + session.install("-r", "requirements-test.txt") + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars() + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session): + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip("SKIPPED: {} tests are disabled for this sample.".format( + session.python + )) + + +# +# Readmegen +# + + +def _get_repo_root(): + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session, path): + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/samples/snippets/requirements-test.txt b/samples/snippets/requirements-test.txt new file mode 100644 index 00000000..be53becf --- /dev/null +++ b/samples/snippets/requirements-test.txt @@ -0,0 +1 @@ +pytest==6.1.1 diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt new file mode 100644 index 00000000..fbe576b0 --- /dev/null +++ b/samples/snippets/requirements.txt @@ -0,0 +1,2 @@ +google-cloud-documentai==0.3.0 +google-cloud-storage==1.32.0 From 5162674091b9a2111b90eb26739b4e11f9119582 Mon Sep 17 00:00:00 2001 From: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Date: Mon, 19 Oct 2020 13:06:02 -0600 Subject: [PATCH 02/30] docs: fix pypi link (#46) 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-documentai/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 # 🦕 --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index f797d7a3..bbf479cd 100644 --- a/README.rst +++ b/README.rst @@ -14,8 +14,8 @@ language, computer vision, translation, and AutoML. :target: https://github.com/googleapis/google-cloud-python/blob/master/README.rst#beta-support .. |pypi| image:: https://img.shields.io/pypi/v/google-cloud-service-directory.svg :target: https://pypi.org/project/google-cloud-service-directory/ -.. |versions| image:: https://img.shields.io/pypi/pyversions/google-cloud-service-directory.svg - :target: https://pypi.org/project/google-cloud-service-directory/ +.. |versions| image:: https://img.shields.io/pypi/pyversions/google-cloud-documentai.svg + :target: https://pypi.org/project/google-cloud-documentai/ .. _Cloud Document AI API: https://cloud.google.com/document-understanding/docs/ .. _Client Library Documentation: https://googleapis.dev/python/documentai/latest .. _Product Documentation: https://cloud.google.com/document-understanding/docs/ @@ -81,4 +81,4 @@ Next Steps APIs that we cover. .. _Cloud Document AI API Product documentation: https://cloud.google.com/document-understanding/docs/ -.. _README: https://github.com/googleapis/google-cloud-python/blob/master/README.rst \ No newline at end of file +.. _README: https://github.com/googleapis/google-cloud-python/blob/master/README.rst From cc8c58d1bade4be53fde08f6a3497eb3f79f63b1 Mon Sep 17 00:00:00 2001 From: aribray <45905583+aribray@users.noreply.github.com> Date: Wed, 21 Oct 2020 15:41:37 -0700 Subject: [PATCH 03/30] docs(samples): new Doc AI samples for v1beta3 (#44) * batch_process_sample. changing from async to synchronous * add quick start and process_document samples and tests * add test and sample for batch_process * add test and sample for batch_process * resolve formatting * use os.environ * remove os.path.join * move tests * descriptive variable * specific Exception, formatting * parse all pages in process_document * add more helpful comments * remove unused imports * better exception handling * rename test files * ran linter, removed nested function in batch predict * refactor tests * format imports * format imports * format imports * serialize as Document object * extract get_text helper function * fix file path * delete test bucket * Update samples/snippets/batch_process_documents_sample_v1beta3_test.py Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> * Update samples/snippets/batch_process_documents_sample_v1beta3_test.py Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> * add more specific assertion in batch_process * add more specific assertion in process_document and quickstart * fix output_uri name * Apply suggestions from code review to resolve exception Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> * resolve exception * lint Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- samples/__init__.py | 0 samples/snippets/__init__.py | 0 .../batch_process_documents_sample_v1beta3.py | 121 ++++++++++++++++++ ...h_process_documents_sample_v1beta3_test.py | 62 +++++++++ samples/snippets/noxfile.py | 29 ++--- .../process_document_sample_v1beta3.py | 88 +++++++++++++ .../process_document_sample_v1beta3_test.py | 37 ++++++ samples/snippets/quickstart_sample_v1beta3.py | 81 ++++++++++++ .../quickstart_sample_v1beta3_test.py | 36 ++++++ samples/snippets/resources/invoice.pdf | Bin 0 -> 58980 bytes 10 files changed, 439 insertions(+), 15 deletions(-) create mode 100644 samples/__init__.py create mode 100644 samples/snippets/__init__.py create mode 100644 samples/snippets/batch_process_documents_sample_v1beta3.py create mode 100644 samples/snippets/batch_process_documents_sample_v1beta3_test.py create mode 100644 samples/snippets/process_document_sample_v1beta3.py create mode 100644 samples/snippets/process_document_sample_v1beta3_test.py create mode 100644 samples/snippets/quickstart_sample_v1beta3.py create mode 100644 samples/snippets/quickstart_sample_v1beta3_test.py create mode 100644 samples/snippets/resources/invoice.pdf diff --git a/samples/__init__.py b/samples/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/samples/snippets/__init__.py b/samples/snippets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/samples/snippets/batch_process_documents_sample_v1beta3.py b/samples/snippets/batch_process_documents_sample_v1beta3.py new file mode 100644 index 00000000..2936b3b3 --- /dev/null +++ b/samples/snippets/batch_process_documents_sample_v1beta3.py @@ -0,0 +1,121 @@ +# 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 documentai_batch_process_document] +import re + +from google.cloud import documentai_v1beta3 as documentai +from google.cloud import storage + +# TODO(developer): Uncomment these variables before running the sample. +# project_id= 'YOUR_PROJECT_ID' +# location = 'YOUR_PROJECT_LOCATION' # Format is 'us' or 'eu' +# processor_id = 'YOUR_PROCESSOR_ID' # Create processor in Cloud Console +# input_uri = "YOUR_INPUT_URI" +# gcs_output_uri = "YOUR_OUTPUT_BUCKET_URI" +# gcs_output_uri_prefix = "YOUR_OUTPUT_URI_PREFIX" + + +def batch_process_documents( + project_id, + location, + processor_id, + gcs_input_uri, + gcs_output_uri, + gcs_output_uri_prefix, +): + + client = documentai.DocumentProcessorServiceClient() + + destination_uri = f"{gcs_output_uri}/{gcs_output_uri_prefix}/" + + # 'mime_type' can be 'application/pdf', 'image/tiff', + # and 'image/gif', or 'application/json' + input_config = documentai.types.document_processor_service.BatchProcessRequest.BatchInputConfig( + gcs_source=gcs_input_uri, mime_type="application/pdf" + ) + + # Where to write results + output_config = documentai.types.document_processor_service.BatchProcessRequest.BatchOutputConfig( + gcs_destination=destination_uri + ) + + # Location can be 'us' or 'eu' + name = f"projects/{project_id}/locations/{location}/processors/{processor_id}" + request = documentai.types.document_processor_service.BatchProcessRequest( + name=name, + input_configs=[input_config], + output_config=output_config, + ) + + operation = client.batch_process_documents(request) + + # Wait for the operation to finish + operation.result() + + # Results are written to GCS. Use a regex to find + # output files + match = re.match(r"gs://([^/]+)/(.+)", destination_uri) + output_bucket = match.group(1) + prefix = match.group(2) + + storage_client = storage.Client() + bucket = storage_client.get_bucket(output_bucket) + blob_list = list(bucket.list_blobs(prefix=prefix)) + print("Output files:") + + for i, blob in enumerate(blob_list): + # Download the contents of this blob as a bytes object. + blob_as_bytes = blob.download_as_bytes() + document = documentai.types.Document.from_json(blob_as_bytes) + + print(f"Fetched file {i + 1}") + + # For a full list of Document object attributes, please reference this page: https://googleapis.dev/python/documentai/latest/_modules/google/cloud/documentai_v1beta3/types/document.html#Document + + # Read the text recognition output from the processor + for page in document.pages: + for form_field in page.form_fields: + field_name = get_text(form_field.field_name, document) + field_value = get_text(form_field.field_value, document) + print("Extracted key value pair:") + print(f"\t{field_name}, {field_value}") + for paragraph in document.pages: + paragraph_text = get_text(paragraph.layout, document) + print(f"Paragraph text:\n{paragraph_text}") + + +# Extract shards from the text field +def get_text(doc_element: dict, document: dict): + """ + Document AI identifies form fields by their offsets + in document text. This function converts offsets + to text snippets. + """ + response = "" + # If a text segment spans several lines, it will + # be stored in different text segments. + for segment in doc_element.text_anchor.text_segments: + start_index = ( + int(segment.start_index) + if "start_index" in doc_element.text_anchor.__dict__ + else 0 + ) + end_index = int(segment.end_index) + response += document.text[start_index:end_index] + return response + + +# [END documentai_batch_process_document] diff --git a/samples/snippets/batch_process_documents_sample_v1beta3_test.py b/samples/snippets/batch_process_documents_sample_v1beta3_test.py new file mode 100644 index 00000000..dcb63567 --- /dev/null +++ b/samples/snippets/batch_process_documents_sample_v1beta3_test.py @@ -0,0 +1,62 @@ +# 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 +from uuid import uuid4 + +from google.cloud import storage +from google.cloud.exceptions import NotFound + +import pytest + +from samples.snippets import batch_process_documents_sample_v1beta3 + +location = "us" +project_id = os.environ["GOOGLE_CLOUD_PROJECT"] +processor_id = "90484cfdedb024f6" +gcs_input_uri = "gs://cloud-samples-data/documentai/invoice.pdf" +gcs_output_uri_prefix = uuid4() +BUCKET_NAME = f"document-ai-python-{uuid4()}" + + +@pytest.fixture(scope="module") +def test_bucket(): + storage_client = storage.Client() + bucket = storage_client.create_bucket(BUCKET_NAME) + yield bucket.name + + try: + blobs = list(bucket.list_blobs()) + for blob in blobs: + blob.delete() + bucket.delete() + except NotFound: + print("Bucket already deleted.") + + +def test_batch_process_documents(capsys, test_bucket): + batch_process_documents_sample_v1beta3.batch_process_documents( + project_id=project_id, + location=location, + processor_id=processor_id, + gcs_input_uri=gcs_input_uri, + gcs_output_uri=f"gs://{test_bucket}", + gcs_output_uri_prefix=gcs_output_uri_prefix, + ) + out, _ = capsys.readouterr() + + assert "Extracted" in out + assert "Paragraph" in out + assert "Invoice" in out diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index eb2b6b9e..817cef92 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,13 +67,13 @@ 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['GCLOUD_PROJECT'] = os.environ[env_key] # deprecated + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + ret["GCLOUD_PROJECT"] = os.environ[env_key] # deprecated # Apply user supplied envs. - ret.update(TEST_CONFIG['envs']) + ret.update(TEST_CONFIG["envs"]) return ret @@ -84,7 +82,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) @@ -147,6 +145,7 @@ def lint(session): # Black # + @nox.session def blacken(session): session.install("black") @@ -194,9 +193,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/process_document_sample_v1beta3.py b/samples/snippets/process_document_sample_v1beta3.py new file mode 100644 index 00000000..330e8183 --- /dev/null +++ b/samples/snippets/process_document_sample_v1beta3.py @@ -0,0 +1,88 @@ +# 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. +# + +from google.cloud import documentai_v1beta3 as documentai + +# [START documentai_process_document] + +# TODO(developer): Uncomment these variables before running the sample. +# project_id= 'YOUR_PROJECT_ID'; +# location = 'YOUR_PROJECT_LOCATION'; // Format is 'us' or 'eu' +# processor_id = 'YOUR_PROCESSOR_ID'; // Create processor in Cloud Console +# file_path = '/path/to/local/pdf'; + + +def process_document_sample( + project_id: str, location: str, processor_id: str, file_path: str +): + # Instantiates a client + client = documentai.DocumentProcessorServiceClient() + + # The full resource name of the processor, e.g.: + # projects/project-id/locations/location/processor/processor-id + # You must create new processors in the Cloud Console first + name = f"projects/{project_id}/locations/{location}/processors/{processor_id}" + + with open(file_path, "rb") as image: + image_content = image.read() + + # Read the file into memory + document = {"content": image_content, "mime_type": "application/pdf"} + + # Configure the process request + request = {"name": name, "document": document} + + # Recognizes text entities in the PDF document + result = client.process_document(request=request) + + document = result.document + + print("Document processing complete.") + + # For a full list of Document object attributes, please reference this page: https://googleapis.dev/python/documentai/latest/_modules/google/cloud/documentai_v1beta3/types/document.html#Document + + document_pages = document.pages + + # Read the text recognition output from the processor + print("The document contains the following paragraphs:") + for page in document_pages: + paragraphs = page.paragraphs + for paragraph in paragraphs: + paragraph_text = get_text(paragraph.layout, document) + print(f"Paragraph text: {paragraph_text}") + + +# Extract shards from the text field +def get_text(doc_element: dict, document: dict): + """ + Document AI identifies form fields by their offsets + in document text. This function converts offsets + to text snippets. + """ + response = "" + # If a text segment spans several lines, it will + # be stored in different text segments. + for segment in doc_element.text_anchor.text_segments: + start_index = ( + int(segment.start_index) + if segment.start_index in doc_element.text_anchor.text_segments + else 0 + ) + end_index = int(segment.end_index) + response += document.text[start_index:end_index] + return response + + +# [END documentai_process_document] diff --git a/samples/snippets/process_document_sample_v1beta3_test.py b/samples/snippets/process_document_sample_v1beta3_test.py new file mode 100644 index 00000000..58b11b22 --- /dev/null +++ b/samples/snippets/process_document_sample_v1beta3_test.py @@ -0,0 +1,37 @@ +# # 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 + +from samples.snippets import process_document_sample_v1beta3 + + +location = "us" +project_id = os.environ["GOOGLE_CLOUD_PROJECT"] +processor_id = "90484cfdedb024f6" +file_path = "resources/invoice.pdf" + + +def test_process_documents(capsys): + process_document_sample_v1beta3.process_document_sample( + project_id=project_id, + location=location, + processor_id=processor_id, + file_path=file_path, + ) + out, _ = capsys.readouterr() + + assert "Paragraph" in out + assert "Invoice" in out diff --git a/samples/snippets/quickstart_sample_v1beta3.py b/samples/snippets/quickstart_sample_v1beta3.py new file mode 100644 index 00000000..c5cd34ae --- /dev/null +++ b/samples/snippets/quickstart_sample_v1beta3.py @@ -0,0 +1,81 @@ +# 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. +# + +from google.cloud import documentai_v1beta3 as documentai + +# [START documentai_quickstart] + +# TODO(developer): Uncomment these variables before running the sample. +# project_id= 'YOUR_PROJECT_ID'; +# location = 'YOUR_PROJECT_LOCATION'; # Format is 'us' or 'eu' +# processor_id = 'YOUR_PROCESSOR_ID'; # Create processor in Cloud Console +# file_path = '/path/to/local/pdf'; + + +def quickstart(project_id: str, location: str, processor_id: str, file_path: str): + client = documentai.DocumentProcessorServiceClient() + + # The full resource name of the processor, e.g.: + # projects/project-id/locations/location/processor/processor-id + # You must create new processors in the Cloud Console first + name = f"projects/{project_id}/locations/{location}/processors/{processor_id}" + + # Read the file into memory + with open(file_path, "rb") as image: + image_content = image.read() + + document = {"content": image_content, "mime_type": "application/pdf"} + + # Configure the process request + request = {"name": name, "document": document} + + result = client.process_document(request=request) + document = result.document + + document_pages = document.pages + + # For a full list of Document object attributes, please reference this page: https://googleapis.dev/python/documentai/latest/_modules/google/cloud/documentai_v1beta3/types/document.html#Document + + # Read the text recognition output from the processor + print("The document contains the following paragraphs:") + for page in document_pages: + paragraphs = page.paragraphs + for paragraph in paragraphs: + print(paragraph) + paragraph_text = get_text(paragraph.layout, document) + print(f"Paragraph text: {paragraph_text}") + + +def get_text(doc_element: dict, document: dict): + """ + Document AI identifies form fields by their offsets + in document text. This function converts offsets + to text snippets. + """ + response = "" + # If a text segment spans several lines, it will + # be stored in different text segments. + for segment in doc_element.text_anchor.text_segments: + start_index = ( + int(segment.start_index) + if segment.start_index in doc_element.text_anchor.text_segments + else 0 + ) + end_index = int(segment.end_index) + response += document.text[start_index:end_index] + return response + + +# [END documentai_quickstart] diff --git a/samples/snippets/quickstart_sample_v1beta3_test.py b/samples/snippets/quickstart_sample_v1beta3_test.py new file mode 100644 index 00000000..4badc1f7 --- /dev/null +++ b/samples/snippets/quickstart_sample_v1beta3_test.py @@ -0,0 +1,36 @@ +# # 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 + +from samples.snippets import quickstart_sample_v1beta3 + +location = "us" +project_id = os.environ["GOOGLE_CLOUD_PROJECT"] +processor_id = "90484cfdedb024f6" +file_path = "resources/invoice.pdf" + + +def test_quickstart(capsys): + quickstart_sample_v1beta3.quickstart( + project_id=project_id, + location=location, + processor_id=processor_id, + file_path=file_path, + ) + out, _ = capsys.readouterr() + + assert "Paragraph" in out + assert "Invoice" in out diff --git a/samples/snippets/resources/invoice.pdf b/samples/snippets/resources/invoice.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7722734a4305b3e2f4f473b2c5783f90062ffdc7 GIT binary patch literal 58980 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%0AE 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 literal 0 HcmV?d00001 From 7039a35e7b9ebbf1f14f4669259560d7100c184c Mon Sep 17 00:00:00 2001 From: Mike <45373284+munkhuushmgl@users.noreply.github.com> Date: Wed, 11 Nov 2020 14:15:02 -0800 Subject: [PATCH 04/30] chores: fixed small issue with start index problem (#56) * chores: fixed small issue with start index problem * added missing update --- samples/snippets/batch_process_documents_sample_v1beta3.py | 2 +- samples/snippets/process_document_sample_v1beta3.py | 2 +- samples/snippets/quickstart_sample_v1beta3.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/snippets/batch_process_documents_sample_v1beta3.py b/samples/snippets/batch_process_documents_sample_v1beta3.py index 2936b3b3..6e22e0ea 100644 --- a/samples/snippets/batch_process_documents_sample_v1beta3.py +++ b/samples/snippets/batch_process_documents_sample_v1beta3.py @@ -110,7 +110,7 @@ def get_text(doc_element: dict, document: dict): for segment in doc_element.text_anchor.text_segments: start_index = ( int(segment.start_index) - if "start_index" in doc_element.text_anchor.__dict__ + if segment in doc_element.text_anchor.text_segments else 0 ) end_index = int(segment.end_index) diff --git a/samples/snippets/process_document_sample_v1beta3.py b/samples/snippets/process_document_sample_v1beta3.py index 330e8183..e666affa 100644 --- a/samples/snippets/process_document_sample_v1beta3.py +++ b/samples/snippets/process_document_sample_v1beta3.py @@ -77,7 +77,7 @@ def get_text(doc_element: dict, document: dict): for segment in doc_element.text_anchor.text_segments: start_index = ( int(segment.start_index) - if segment.start_index in doc_element.text_anchor.text_segments + if segment in doc_element.text_anchor.text_segments else 0 ) end_index = int(segment.end_index) diff --git a/samples/snippets/quickstart_sample_v1beta3.py b/samples/snippets/quickstart_sample_v1beta3.py index c5cd34ae..7eb4a8ff 100644 --- a/samples/snippets/quickstart_sample_v1beta3.py +++ b/samples/snippets/quickstart_sample_v1beta3.py @@ -70,7 +70,7 @@ def get_text(doc_element: dict, document: dict): for segment in doc_element.text_anchor.text_segments: start_index = ( int(segment.start_index) - if segment.start_index in doc_element.text_anchor.text_segments + if segment in doc_element.text_anchor.text_segments else 0 ) end_index = int(segment.end_index) From 4918e62033b4c118bf99ba83730377b4ecc86d17 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 17 Nov 2020 16:58:33 -0800 Subject: [PATCH 05/30] feat: add common resource path helpers, expose client transport (#43) --- .kokoro/docs/common.cfg | 2 +- .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 +- CODE_OF_CONDUCT.md | 123 +++++--- docs/conf.py | 1 + docs/documentai_v1beta2/types.rst | 1 + docs/documentai_v1beta3/types.rst | 1 + .../async_client.py | 55 +++- .../document_understanding_service/client.py | 80 +++++- .../transports/base.py | 4 +- .../transports/grpc.py | 18 +- .../transports/grpc_asyncio.py | 4 + .../documentai_v1beta2/types/geometry.py | 4 +- .../async_client.py | 68 ++++- .../document_processor_service/client.py | 108 ++++++- .../transports/base.py | 6 +- .../transports/grpc.py | 18 +- .../transports/grpc_asyncio.py | 4 + .../documentai_v1beta3/types/geometry.py | 4 +- noxfile.py | 6 +- scripts/fixup_documentai_v1beta2_keywords.py | 1 + scripts/fixup_documentai_v1beta3_keywords.py | 1 + synth.metadata | 14 +- synth.py | 2 +- .../test_document_understanding_service.py | 181 +++++++++--- .../test_document_processor_service.py | 271 ++++++++++++++---- 28 files changed, 824 insertions(+), 179 deletions(-) diff --git a/.kokoro/docs/common.cfg b/.kokoro/docs/common.cfg index fcc98d5e..c90da11c 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/samples/python3.6/common.cfg b/.kokoro/samples/python3.6/common.cfg index f754d781..a042a54a 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-documentai/.kokoro/test-samples.sh" diff --git a/.kokoro/samples/python3.7/common.cfg b/.kokoro/samples/python3.7/common.cfg index ac8e6e0a..10b51166 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-documentai/.kokoro/test-samples.sh" diff --git a/.kokoro/samples/python3.8/common.cfg b/.kokoro/samples/python3.8/common.cfg index 72a772e8..9a69601d 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-documentai/.kokoro/test-samples.sh" diff --git a/.kokoro/test-samples.sh b/.kokoro/test-samples.sh index 557116a9..f97019c6 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/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 d5ee4abd..6d80d2bf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -349,6 +349,7 @@ "google-auth": ("https://google-auth.readthedocs.io/en/stable", None), "google.api_core": ("https://googleapis.dev/python/google-api-core/latest/", None,), "grpc": ("https://grpc.io/grpc/python/", None), + "proto-plus": ("https://proto-plus-python.readthedocs.io/en/latest/", None), } diff --git a/docs/documentai_v1beta2/types.rst b/docs/documentai_v1beta2/types.rst index 2a437e9d..35540dd0 100644 --- a/docs/documentai_v1beta2/types.rst +++ b/docs/documentai_v1beta2/types.rst @@ -3,3 +3,4 @@ Types for Google Cloud Documentai v1beta2 API .. automodule:: google.cloud.documentai_v1beta2.types :members: + :show-inheritance: diff --git a/docs/documentai_v1beta3/types.rst b/docs/documentai_v1beta3/types.rst index 03bcbfa7..31b489da 100644 --- a/docs/documentai_v1beta3/types.rst +++ b/docs/documentai_v1beta3/types.rst @@ -3,3 +3,4 @@ Types for Google Cloud Documentai v1beta3 API .. automodule:: google.cloud.documentai_v1beta3.types :members: + :show-inheritance: diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py index a293e4b2..c961662b 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py @@ -50,11 +50,55 @@ class DocumentUnderstandingServiceAsyncClient: DEFAULT_ENDPOINT = DocumentUnderstandingServiceClient.DEFAULT_ENDPOINT DEFAULT_MTLS_ENDPOINT = DocumentUnderstandingServiceClient.DEFAULT_MTLS_ENDPOINT + common_billing_account_path = staticmethod( + DocumentUnderstandingServiceClient.common_billing_account_path + ) + parse_common_billing_account_path = staticmethod( + DocumentUnderstandingServiceClient.parse_common_billing_account_path + ) + + common_folder_path = staticmethod( + DocumentUnderstandingServiceClient.common_folder_path + ) + parse_common_folder_path = staticmethod( + DocumentUnderstandingServiceClient.parse_common_folder_path + ) + + common_organization_path = staticmethod( + DocumentUnderstandingServiceClient.common_organization_path + ) + parse_common_organization_path = staticmethod( + DocumentUnderstandingServiceClient.parse_common_organization_path + ) + + common_project_path = staticmethod( + DocumentUnderstandingServiceClient.common_project_path + ) + parse_common_project_path = staticmethod( + DocumentUnderstandingServiceClient.parse_common_project_path + ) + + common_location_path = staticmethod( + DocumentUnderstandingServiceClient.common_location_path + ) + parse_common_location_path = staticmethod( + DocumentUnderstandingServiceClient.parse_common_location_path + ) + from_service_account_file = ( DocumentUnderstandingServiceClient.from_service_account_file ) from_service_account_json = from_service_account_file + @property + def transport(self) -> DocumentUnderstandingServiceTransport: + """Return the transport used by the client instance. + + Returns: + DocumentUnderstandingServiceTransport: The transport used by the client instance. + """ + return self._client.transport + get_transport_class = functools.partial( type(DocumentUnderstandingServiceClient).get_transport_class, type(DocumentUnderstandingServiceClient), @@ -152,7 +196,8 @@ async def batch_process_documents( # Create or coerce a protobuf request object. # Sanity check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. - if request is not None and any([requests]): + has_flattened_params = any([requests]) + if request is not None and has_flattened_params: raise ValueError( "If the `request` argument is set, then none of " "the individual field arguments should be set." @@ -163,8 +208,8 @@ async def batch_process_documents( # If we have keyword arguments corresponding to fields on the # request, apply these. - if requests is not None: - request.requests = requests + if requests: + request.requests.extend(requests) # Wrap the RPC method; this adds retry and timeout information, # and friendly error handling. @@ -175,7 +220,7 @@ async def batch_process_documents( maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), ), default_timeout=120.0, @@ -246,7 +291,7 @@ async def process_document( maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), ), default_timeout=120.0, diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py index 433de5c1..0d740e5b 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py @@ -142,6 +142,74 @@ def from_service_account_file(cls, filename: str, *args, **kwargs): from_service_account_json = from_service_account_file + @property + def transport(self) -> DocumentUnderstandingServiceTransport: + """Return the transport used by the client instance. + + Returns: + DocumentUnderstandingServiceTransport: The transport used by the client instance. + """ + return self._transport + + @staticmethod + def common_billing_account_path(billing_account: str,) -> str: + """Return a fully-qualified billing_account string.""" + return "billingAccounts/{billing_account}".format( + billing_account=billing_account, + ) + + @staticmethod + def parse_common_billing_account_path(path: str) -> Dict[str, str]: + """Parse a billing_account path into its component segments.""" + m = re.match(r"^billingAccounts/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_folder_path(folder: str,) -> str: + """Return a fully-qualified folder string.""" + return "folders/{folder}".format(folder=folder,) + + @staticmethod + def parse_common_folder_path(path: str) -> Dict[str, str]: + """Parse a folder path into its component segments.""" + m = re.match(r"^folders/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_organization_path(organization: str,) -> str: + """Return a fully-qualified organization string.""" + return "organizations/{organization}".format(organization=organization,) + + @staticmethod + def parse_common_organization_path(path: str) -> Dict[str, str]: + """Parse a organization path into its component segments.""" + m = re.match(r"^organizations/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_project_path(project: str,) -> str: + """Return a fully-qualified project string.""" + return "projects/{project}".format(project=project,) + + @staticmethod + def parse_common_project_path(path: str) -> Dict[str, str]: + """Parse a project path into its component segments.""" + m = re.match(r"^projects/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_location_path(project: str, location: str,) -> str: + """Return a fully-qualified location string.""" + return "projects/{project}/locations/{location}".format( + project=project, location=location, + ) + + @staticmethod + def parse_common_location_path(path: str) -> Dict[str, str]: + """Parse a location path into its component segments.""" + m = re.match(r"^projects/(?P.+?)/locations/(?P.+?)$", path) + return m.groupdict() if m else {} + def __init__( self, *, @@ -177,10 +245,10 @@ def __init__( 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 + 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: @@ -320,8 +388,8 @@ def batch_process_documents( # If we have keyword arguments corresponding to fields on the # request, apply these. - if requests is not None: - request.requests = requests + if requests: + request.requests.extend(requests) # Wrap the RPC method; this adds retry and timeout information, # and friendly error handling. diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/base.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/base.py index df52dbcc..547c5803 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/base.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/base.py @@ -116,7 +116,7 @@ def _prep_wrapped_messages(self, client_info): maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), ), default_timeout=120.0, @@ -129,7 +129,7 @@ def _prep_wrapped_messages(self, client_info): maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), ), default_timeout=120.0, diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py index 60f3e8b8..0390ff51 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py @@ -94,10 +94,10 @@ def __init__( 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 + 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: @@ -106,6 +106,8 @@ def __init__( google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` and ``credentials_file`` are passed. """ + self._ssl_channel_credentials = ssl_channel_credentials + if channel: # Sanity check: Ensure that channel and credentials are not both # provided. @@ -113,6 +115,7 @@ def __init__( # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._ssl_channel_credentials = None elif api_mtls_endpoint: warnings.warn( "api_mtls_endpoint and client_cert_source are deprecated", @@ -149,6 +152,7 @@ def __init__( scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, ) + self._ssl_channel_credentials = ssl_credentials else: host = host if ":" in host else host + ":443" @@ -226,12 +230,8 @@ def create_channel( @property def grpc_channel(self) -> grpc.Channel: - """Create the channel designed to connect to this service. - - This property caches on the instance; repeated calls return - the same channel. + """Return the channel designed to connect to this service. """ - # Return the channel from cache. return self._grpc_channel @property diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py index 315795e5..95122515 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py @@ -153,6 +153,8 @@ def __init__( google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` and ``credentials_file`` are passed. """ + self._ssl_channel_credentials = ssl_channel_credentials + if channel: # Sanity check: Ensure that channel and credentials are not both # provided. @@ -160,6 +162,7 @@ def __init__( # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._ssl_channel_credentials = None elif api_mtls_endpoint: warnings.warn( "api_mtls_endpoint and client_cert_source are deprecated", @@ -196,6 +199,7 @@ def __init__( scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, ) + self._ssl_channel_credentials = ssl_credentials else: host = host if ":" in host else host + ":443" diff --git a/google/cloud/documentai_v1beta2/types/geometry.py b/google/cloud/documentai_v1beta2/types/geometry.py index 12d63f90..0592f336 100644 --- a/google/cloud/documentai_v1beta2/types/geometry.py +++ b/google/cloud/documentai_v1beta2/types/geometry.py @@ -68,10 +68,10 @@ class BoundingPoly(proto.Message): The bounding polygon normalized vertices. """ - vertices = proto.RepeatedField(proto.MESSAGE, number=1, message=Vertex,) + vertices = proto.RepeatedField(proto.MESSAGE, number=1, message="Vertex",) normalized_vertices = proto.RepeatedField( - proto.MESSAGE, number=2, message=NormalizedVertex, + proto.MESSAGE, number=2, message="NormalizedVertex", ) diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py b/google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py index 7ba80ac2..467b072a 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py @@ -51,9 +51,62 @@ class DocumentProcessorServiceAsyncClient: DEFAULT_ENDPOINT = DocumentProcessorServiceClient.DEFAULT_ENDPOINT DEFAULT_MTLS_ENDPOINT = DocumentProcessorServiceClient.DEFAULT_MTLS_ENDPOINT + human_review_config_path = staticmethod( + DocumentProcessorServiceClient.human_review_config_path + ) + parse_human_review_config_path = staticmethod( + DocumentProcessorServiceClient.parse_human_review_config_path + ) + processor_path = staticmethod(DocumentProcessorServiceClient.processor_path) + parse_processor_path = staticmethod( + DocumentProcessorServiceClient.parse_processor_path + ) + + common_billing_account_path = staticmethod( + DocumentProcessorServiceClient.common_billing_account_path + ) + parse_common_billing_account_path = staticmethod( + DocumentProcessorServiceClient.parse_common_billing_account_path + ) + + common_folder_path = staticmethod(DocumentProcessorServiceClient.common_folder_path) + parse_common_folder_path = staticmethod( + DocumentProcessorServiceClient.parse_common_folder_path + ) + + common_organization_path = staticmethod( + DocumentProcessorServiceClient.common_organization_path + ) + parse_common_organization_path = staticmethod( + DocumentProcessorServiceClient.parse_common_organization_path + ) + + common_project_path = staticmethod( + DocumentProcessorServiceClient.common_project_path + ) + parse_common_project_path = staticmethod( + DocumentProcessorServiceClient.parse_common_project_path + ) + + common_location_path = staticmethod( + DocumentProcessorServiceClient.common_location_path + ) + parse_common_location_path = staticmethod( + DocumentProcessorServiceClient.parse_common_location_path + ) + from_service_account_file = DocumentProcessorServiceClient.from_service_account_file from_service_account_json = from_service_account_file + @property + def transport(self) -> DocumentProcessorServiceTransport: + """Return the transport used by the client instance. + + Returns: + DocumentProcessorServiceTransport: The transport used by the client instance. + """ + return self._client.transport + get_transport_class = functools.partial( type(DocumentProcessorServiceClient).get_transport_class, type(DocumentProcessorServiceClient), @@ -144,7 +197,8 @@ async def process_document( # Create or coerce a protobuf request object. # Sanity check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. - if request is not None and any([name]): + has_flattened_params = any([name]) + if request is not None and has_flattened_params: raise ValueError( "If the `request` argument is set, then none of " "the individual field arguments should be set." @@ -167,7 +221,7 @@ async def process_document( maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), ), default_timeout=120.0, @@ -227,7 +281,8 @@ async def batch_process_documents( # Create or coerce a protobuf request object. # Sanity check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. - if request is not None and any([name]): + has_flattened_params = any([name]) + if request is not None and has_flattened_params: raise ValueError( "If the `request` argument is set, then none of " "the individual field arguments should be set." @@ -250,7 +305,7 @@ async def batch_process_documents( maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), ), default_timeout=120.0, @@ -319,7 +374,8 @@ async def review_document( # Create or coerce a protobuf request object. # Sanity check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. - if request is not None and any([human_review_config]): + has_flattened_params = any([human_review_config]) + if request is not None and has_flattened_params: raise ValueError( "If the `request` argument is set, then none of " "the individual field arguments should be set." @@ -342,7 +398,7 @@ async def review_document( maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), ), default_timeout=120.0, diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/client.py b/google/cloud/documentai_v1beta3/services/document_processor_service/client.py index 84b57f36..e621e251 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/client.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/client.py @@ -139,6 +139,106 @@ def from_service_account_file(cls, filename: str, *args, **kwargs): from_service_account_json = from_service_account_file + @property + def transport(self) -> DocumentProcessorServiceTransport: + """Return the transport used by the client instance. + + Returns: + DocumentProcessorServiceTransport: The transport used by the client instance. + """ + return self._transport + + @staticmethod + def human_review_config_path(project: str, location: str, processor: str,) -> str: + """Return a fully-qualified human_review_config string.""" + return "projects/{project}/locations/{location}/processors/{processor}/humanReviewConfig".format( + project=project, location=location, processor=processor, + ) + + @staticmethod + def parse_human_review_config_path(path: str) -> Dict[str, str]: + """Parse a human_review_config path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/processors/(?P.+?)/humanReviewConfig$", + path, + ) + return m.groupdict() if m else {} + + @staticmethod + def processor_path(project: str, location: str, processor: str,) -> str: + """Return a fully-qualified processor string.""" + return "projects/{project}/locations/{location}/processors/{processor}".format( + project=project, location=location, processor=processor, + ) + + @staticmethod + def parse_processor_path(path: str) -> Dict[str, str]: + """Parse a processor path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/processors/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + + @staticmethod + def common_billing_account_path(billing_account: str,) -> str: + """Return a fully-qualified billing_account string.""" + return "billingAccounts/{billing_account}".format( + billing_account=billing_account, + ) + + @staticmethod + def parse_common_billing_account_path(path: str) -> Dict[str, str]: + """Parse a billing_account path into its component segments.""" + m = re.match(r"^billingAccounts/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_folder_path(folder: str,) -> str: + """Return a fully-qualified folder string.""" + return "folders/{folder}".format(folder=folder,) + + @staticmethod + def parse_common_folder_path(path: str) -> Dict[str, str]: + """Parse a folder path into its component segments.""" + m = re.match(r"^folders/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_organization_path(organization: str,) -> str: + """Return a fully-qualified organization string.""" + return "organizations/{organization}".format(organization=organization,) + + @staticmethod + def parse_common_organization_path(path: str) -> Dict[str, str]: + """Parse a organization path into its component segments.""" + m = re.match(r"^organizations/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_project_path(project: str,) -> str: + """Return a fully-qualified project string.""" + return "projects/{project}".format(project=project,) + + @staticmethod + def parse_common_project_path(path: str) -> Dict[str, str]: + """Parse a project path into its component segments.""" + m = re.match(r"^projects/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_location_path(project: str, location: str,) -> str: + """Return a fully-qualified location string.""" + return "projects/{project}/locations/{location}".format( + project=project, location=location, + ) + + @staticmethod + def parse_common_location_path(path: str) -> Dict[str, str]: + """Parse a location path into its component segments.""" + m = re.match(r"^projects/(?P.+?)/locations/(?P.+?)$", path) + return m.groupdict() if m else {} + def __init__( self, *, @@ -174,10 +274,10 @@ def __init__( 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 + 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: diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/base.py b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/base.py index dfa32e24..e24d4922 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/base.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/base.py @@ -115,7 +115,7 @@ def _prep_wrapped_messages(self, client_info): maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), ), default_timeout=120.0, @@ -128,7 +128,7 @@ def _prep_wrapped_messages(self, client_info): maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), ), default_timeout=120.0, @@ -141,7 +141,7 @@ def _prep_wrapped_messages(self, client_info): maximum=60.0, multiplier=1.3, predicate=retries.if_exception_type( - exceptions.ServiceUnavailable, exceptions.DeadlineExceeded, + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), ), default_timeout=120.0, diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py index d7220126..435767b0 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py @@ -95,10 +95,10 @@ def __init__( 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 + 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: @@ -107,6 +107,8 @@ def __init__( google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` and ``credentials_file`` are passed. """ + self._ssl_channel_credentials = ssl_channel_credentials + if channel: # Sanity check: Ensure that channel and credentials are not both # provided. @@ -114,6 +116,7 @@ def __init__( # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._ssl_channel_credentials = None elif api_mtls_endpoint: warnings.warn( "api_mtls_endpoint and client_cert_source are deprecated", @@ -150,6 +153,7 @@ def __init__( scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, ) + self._ssl_channel_credentials = ssl_credentials else: host = host if ":" in host else host + ":443" @@ -227,12 +231,8 @@ def create_channel( @property def grpc_channel(self) -> grpc.Channel: - """Create the channel designed to connect to this service. - - This property caches on the instance; repeated calls return - the same channel. + """Return the channel designed to connect to this service. """ - # Return the channel from cache. return self._grpc_channel @property diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py index 391819cf..df9be8a8 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py @@ -152,6 +152,8 @@ def __init__( google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` and ``credentials_file`` are passed. """ + self._ssl_channel_credentials = ssl_channel_credentials + if channel: # Sanity check: Ensure that channel and credentials are not both # provided. @@ -159,6 +161,7 @@ def __init__( # If a channel was explicitly provided, set it. self._grpc_channel = channel + self._ssl_channel_credentials = None elif api_mtls_endpoint: warnings.warn( "api_mtls_endpoint and client_cert_source are deprecated", @@ -195,6 +198,7 @@ def __init__( scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, ) + self._ssl_channel_credentials = ssl_credentials else: host = host if ":" in host else host + ":443" diff --git a/google/cloud/documentai_v1beta3/types/geometry.py b/google/cloud/documentai_v1beta3/types/geometry.py index e87b87c7..b72dab75 100644 --- a/google/cloud/documentai_v1beta3/types/geometry.py +++ b/google/cloud/documentai_v1beta3/types/geometry.py @@ -68,10 +68,10 @@ class BoundingPoly(proto.Message): The bounding polygon normalized vertices. """ - vertices = proto.RepeatedField(proto.MESSAGE, number=1, message=Vertex,) + vertices = proto.RepeatedField(proto.MESSAGE, number=1, message="Vertex",) normalized_vertices = proto.RepeatedField( - proto.MESSAGE, number=2, message=NormalizedVertex, + proto.MESSAGE, number=2, message="NormalizedVertex", ) diff --git a/noxfile.py b/noxfile.py index e446dd8d..d531ac69 100644 --- a/noxfile.py +++ b/noxfile.py @@ -28,7 +28,7 @@ DEFAULT_PYTHON_VERSION = "3.8" SYSTEM_TEST_PYTHON_VERSIONS = ["3.8"] -UNIT_TEST_PYTHON_VERSIONS = ["3.6", "3.7", "3.8"] +UNIT_TEST_PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9"] @nox.session(python=DEFAULT_PYTHON_VERSION) @@ -72,7 +72,9 @@ def default(session): # Install all test dependencies, then install this package in-place. session.install("asyncmock", "pytest-asyncio") - session.install("mock", "pytest", "pytest-cov") + session.install( + "mock", "pytest", "pytest-cov", + ) session.install("-e", ".") # Run py.test against the unit tests. diff --git a/scripts/fixup_documentai_v1beta2_keywords.py b/scripts/fixup_documentai_v1beta2_keywords.py index 0cb9fcbf..d2f24146 100644 --- a/scripts/fixup_documentai_v1beta2_keywords.py +++ b/scripts/fixup_documentai_v1beta2_keywords.py @@ -1,3 +1,4 @@ +#! /usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright 2020 Google LLC diff --git a/scripts/fixup_documentai_v1beta3_keywords.py b/scripts/fixup_documentai_v1beta3_keywords.py index 2b689522..750630f1 100644 --- a/scripts/fixup_documentai_v1beta3_keywords.py +++ b/scripts/fixup_documentai_v1beta3_keywords.py @@ -1,3 +1,4 @@ +#! /usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright 2020 Google LLC diff --git a/synth.metadata b/synth.metadata index 3a7962ec..8088f01c 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,21 +4,29 @@ "git": { "name": ".", "remote": "git@github.com:googleapis/python-documentai", - "sha": "ec70a8cec0f1fbd0f8ec18189139e632ec28b025" + "sha": "c6186ea7a58f83bc2e49d9df2a48fce3f78f0143" + } + }, + { + "git": { + "name": "googleapis", + "remote": "https://github.com/googleapis/googleapis.git", + "sha": "e3e7e7ddb0fecd7bc61ca03b5a9ddb29cc9b48d8", + "internalRef": "342967619" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "e6168630be3e31eede633ba2c6f1cd64248dec1c" + "sha": "7fcc405a579d5d53a726ff3da1b7c8c08f0f2d58" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "e6168630be3e31eede633ba2c6f1cd64248dec1c" + "sha": "7fcc405a579d5d53a726ff3da1b7c8c08f0f2d58" } } ], diff --git a/synth.py b/synth.py index 3ae8445c..3281f52f 100644 --- a/synth.py +++ b/synth.py @@ -54,6 +54,6 @@ excludes=[".coveragerc"], # microgenerator has a good .coveragerc file ) -python.py_samples() +python.py_samples(skip_readmes=True) s.shell.run(["nox", "-s", "blacken"], hide_output=False) diff --git a/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py b/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py index 290cb2f2..17fdb324 100644 --- a/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py +++ b/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py @@ -110,12 +110,12 @@ def test_document_understanding_service_client_from_service_account_file(client_ ) as factory: factory.return_value = creds client = client_class.from_service_account_file("dummy/file/path.json") - assert client._transport._credentials == creds + assert client.transport._credentials == creds client = client_class.from_service_account_json("dummy/file/path.json") - assert client._transport._credentials == creds + assert client.transport._credentials == creds - assert client._transport._host == "us-documentai.googleapis.com:443" + assert client.transport._host == "us-documentai.googleapis.com:443" def test_document_understanding_service_client_get_transport_class(): @@ -493,7 +493,7 @@ def test_batch_process_documents( # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._transport.batch_process_documents), "__call__" + type(client.transport.batch_process_documents), "__call__" ) as call: # Designate an appropriate return value for the call. call.return_value = operations_pb2.Operation(name="operations/spam") @@ -515,18 +515,21 @@ def test_batch_process_documents_from_dict(): @pytest.mark.asyncio -async def test_batch_process_documents_async(transport: str = "grpc_asyncio"): +async def test_batch_process_documents_async( + transport: str = "grpc_asyncio", + request_type=document_understanding.BatchProcessDocumentsRequest, +): client = DocumentUnderstandingServiceAsyncClient( credentials=credentials.AnonymousCredentials(), transport=transport, ) # Everything is optional in proto3 as far as the runtime is concerned, # and we are mocking out the actual API, so just send an empty request. - request = document_understanding.BatchProcessDocumentsRequest() + request = request_type() # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client._transport.batch_process_documents), "__call__" + type(client.transport.batch_process_documents), "__call__" ) as call: # Designate an appropriate return value for the call. call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( @@ -539,12 +542,17 @@ async def test_batch_process_documents_async(transport: str = "grpc_asyncio"): assert len(call.mock_calls) _, args, _ = call.mock_calls[0] - assert args[0] == request + assert args[0] == document_understanding.BatchProcessDocumentsRequest() # Establish that the response is the type that we expect. assert isinstance(response, future.Future) +@pytest.mark.asyncio +async def test_batch_process_documents_async_from_dict(): + await test_batch_process_documents_async(request_type=dict) + + def test_batch_process_documents_field_headers(): client = DocumentUnderstandingServiceClient( credentials=credentials.AnonymousCredentials(), @@ -557,7 +565,7 @@ def test_batch_process_documents_field_headers(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._transport.batch_process_documents), "__call__" + type(client.transport.batch_process_documents), "__call__" ) as call: call.return_value = operations_pb2.Operation(name="operations/op") @@ -586,7 +594,7 @@ async def test_batch_process_documents_field_headers_async(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client._transport.batch_process_documents), "__call__" + type(client.transport.batch_process_documents), "__call__" ) as call: call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( operations_pb2.Operation(name="operations/op") @@ -611,7 +619,7 @@ def test_batch_process_documents_flattened(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._transport.batch_process_documents), "__call__" + type(client.transport.batch_process_documents), "__call__" ) as call: # Designate an appropriate return value for the call. call.return_value = operations_pb2.Operation(name="operations/op") @@ -658,7 +666,7 @@ async def test_batch_process_documents_flattened_async(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client._transport.batch_process_documents), "__call__" + type(client.transport.batch_process_documents), "__call__" ) as call: # Designate an appropriate return value for the call. call.return_value = operations_pb2.Operation(name="operations/op") @@ -713,9 +721,7 @@ def test_process_document( request = request_type() # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object( - type(client._transport.process_document), "__call__" - ) as call: + with mock.patch.object(type(client.transport.process_document), "__call__") as call: # Designate an appropriate return value for the call. call.return_value = document.Document( mime_type="mime_type_value", text="text_value", uri="uri_value", @@ -730,6 +736,7 @@ def test_process_document( assert args[0] == document_understanding.ProcessDocumentRequest() # Establish that the response is the type that we expect. + assert isinstance(response, document.Document) assert response.mime_type == "mime_type_value" @@ -742,19 +749,20 @@ def test_process_document_from_dict(): @pytest.mark.asyncio -async def test_process_document_async(transport: str = "grpc_asyncio"): +async def test_process_document_async( + transport: str = "grpc_asyncio", + request_type=document_understanding.ProcessDocumentRequest, +): client = DocumentUnderstandingServiceAsyncClient( credentials=credentials.AnonymousCredentials(), transport=transport, ) # Everything is optional in proto3 as far as the runtime is concerned, # and we are mocking out the actual API, so just send an empty request. - request = document_understanding.ProcessDocumentRequest() + request = request_type() # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object( - type(client._client._transport.process_document), "__call__" - ) as call: + with mock.patch.object(type(client.transport.process_document), "__call__") as call: # Designate an appropriate return value for the call. call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( document.Document(mime_type="mime_type_value", text="text_value",) @@ -766,7 +774,7 @@ async def test_process_document_async(transport: str = "grpc_asyncio"): assert len(call.mock_calls) _, args, _ = call.mock_calls[0] - assert args[0] == request + assert args[0] == document_understanding.ProcessDocumentRequest() # Establish that the response is the type that we expect. assert isinstance(response, document.Document) @@ -776,6 +784,11 @@ async def test_process_document_async(transport: str = "grpc_asyncio"): assert response.text == "text_value" +@pytest.mark.asyncio +async def test_process_document_async_from_dict(): + await test_process_document_async(request_type=dict) + + def test_process_document_field_headers(): client = DocumentUnderstandingServiceClient( credentials=credentials.AnonymousCredentials(), @@ -787,9 +800,7 @@ def test_process_document_field_headers(): request.parent = "parent/value" # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object( - type(client._transport.process_document), "__call__" - ) as call: + with mock.patch.object(type(client.transport.process_document), "__call__") as call: call.return_value = document.Document() client.process_document(request) @@ -816,9 +827,7 @@ async def test_process_document_field_headers_async(): request.parent = "parent/value" # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object( - type(client._client._transport.process_document), "__call__" - ) as call: + with mock.patch.object(type(client.transport.process_document), "__call__") as call: call.return_value = grpc_helpers_async.FakeUnaryUnaryCall(document.Document()) await client.process_document(request) @@ -869,7 +878,7 @@ def test_transport_instance(): credentials=credentials.AnonymousCredentials(), ) client = DocumentUnderstandingServiceClient(transport=transport) - assert client._transport is transport + assert client.transport is transport def test_transport_get_channel(): @@ -908,7 +917,7 @@ def test_transport_grpc_default(): credentials=credentials.AnonymousCredentials(), ) assert isinstance( - client._transport, transports.DocumentUnderstandingServiceGrpcTransport, + client.transport, transports.DocumentUnderstandingServiceGrpcTransport, ) @@ -1009,7 +1018,7 @@ def test_document_understanding_service_host_no_port(): api_endpoint="us-documentai.googleapis.com" ), ) - assert client._transport._host == "us-documentai.googleapis.com:443" + assert client.transport._host == "us-documentai.googleapis.com:443" def test_document_understanding_service_host_with_port(): @@ -1019,7 +1028,7 @@ def test_document_understanding_service_host_with_port(): api_endpoint="us-documentai.googleapis.com:8000" ), ) - assert client._transport._host == "us-documentai.googleapis.com:8000" + assert client.transport._host == "us-documentai.googleapis.com:8000" def test_document_understanding_service_grpc_transport_channel(): @@ -1031,6 +1040,7 @@ def test_document_understanding_service_grpc_transport_channel(): ) assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" + assert transport._ssl_channel_credentials == None def test_document_understanding_service_grpc_asyncio_transport_channel(): @@ -1042,6 +1052,7 @@ def test_document_understanding_service_grpc_asyncio_transport_channel(): ) assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" + assert transport._ssl_channel_credentials == None @pytest.mark.parametrize( @@ -1089,6 +1100,7 @@ def test_document_understanding_service_transport_channel_mtls_with_client_cert_ quota_project_id=None, ) assert transport.grpc_channel == mock_grpc_channel + assert transport._ssl_channel_credentials == mock_ssl_cred @pytest.mark.parametrize( @@ -1137,7 +1149,7 @@ def test_document_understanding_service_grpc_lro_client(): client = DocumentUnderstandingServiceClient( credentials=credentials.AnonymousCredentials(), transport="grpc", ) - transport = client._transport + transport = client.transport # Ensure that we have a api-core operations client. assert isinstance(transport.operations_client, operations_v1.OperationsClient,) @@ -1150,7 +1162,7 @@ def test_document_understanding_service_grpc_lro_async_client(): client = DocumentUnderstandingServiceAsyncClient( credentials=credentials.AnonymousCredentials(), transport="grpc_asyncio", ) - transport = client._client._transport + transport = client.transport # Ensure that we have a api-core operations client. assert isinstance(transport.operations_client, operations_v1.OperationsAsyncClient,) @@ -1159,6 +1171,109 @@ def test_document_understanding_service_grpc_lro_async_client(): assert transport.operations_client is transport.operations_client +def test_common_billing_account_path(): + billing_account = "squid" + + expected = "billingAccounts/{billing_account}".format( + billing_account=billing_account, + ) + actual = DocumentUnderstandingServiceClient.common_billing_account_path( + billing_account + ) + assert expected == actual + + +def test_parse_common_billing_account_path(): + expected = { + "billing_account": "clam", + } + path = DocumentUnderstandingServiceClient.common_billing_account_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentUnderstandingServiceClient.parse_common_billing_account_path(path) + assert expected == actual + + +def test_common_folder_path(): + folder = "whelk" + + expected = "folders/{folder}".format(folder=folder,) + actual = DocumentUnderstandingServiceClient.common_folder_path(folder) + assert expected == actual + + +def test_parse_common_folder_path(): + expected = { + "folder": "octopus", + } + path = DocumentUnderstandingServiceClient.common_folder_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentUnderstandingServiceClient.parse_common_folder_path(path) + assert expected == actual + + +def test_common_organization_path(): + organization = "oyster" + + expected = "organizations/{organization}".format(organization=organization,) + actual = DocumentUnderstandingServiceClient.common_organization_path(organization) + assert expected == actual + + +def test_parse_common_organization_path(): + expected = { + "organization": "nudibranch", + } + path = DocumentUnderstandingServiceClient.common_organization_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentUnderstandingServiceClient.parse_common_organization_path(path) + assert expected == actual + + +def test_common_project_path(): + project = "cuttlefish" + + expected = "projects/{project}".format(project=project,) + actual = DocumentUnderstandingServiceClient.common_project_path(project) + assert expected == actual + + +def test_parse_common_project_path(): + expected = { + "project": "mussel", + } + path = DocumentUnderstandingServiceClient.common_project_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentUnderstandingServiceClient.parse_common_project_path(path) + assert expected == actual + + +def test_common_location_path(): + project = "winkle" + location = "nautilus" + + expected = "projects/{project}/locations/{location}".format( + project=project, location=location, + ) + actual = DocumentUnderstandingServiceClient.common_location_path(project, location) + assert expected == actual + + +def test_parse_common_location_path(): + expected = { + "project": "scallop", + "location": "abalone", + } + path = DocumentUnderstandingServiceClient.common_location_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentUnderstandingServiceClient.parse_common_location_path(path) + assert expected == actual + + def test_client_withDEFAULT_CLIENT_INFO(): client_info = gapic_v1.client_info.ClientInfo() diff --git a/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py b/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py index 4b17a5ed..ad4346e8 100644 --- a/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py +++ b/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py @@ -117,12 +117,12 @@ def test_document_processor_service_client_from_service_account_file(client_clas ) as factory: factory.return_value = creds client = client_class.from_service_account_file("dummy/file/path.json") - assert client._transport._credentials == creds + assert client.transport._credentials == creds client = client_class.from_service_account_json("dummy/file/path.json") - assert client._transport._credentials == creds + assert client.transport._credentials == creds - assert client._transport._host == "us-documentai.googleapis.com:443" + assert client.transport._host == "us-documentai.googleapis.com:443" def test_document_processor_service_client_get_transport_class(): @@ -498,9 +498,7 @@ def test_process_document( request = request_type() # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object( - type(client._transport.process_document), "__call__" - ) as call: + with mock.patch.object(type(client.transport.process_document), "__call__") as call: # Designate an appropriate return value for the call. call.return_value = document_processor_service.ProcessResponse( human_review_operation="human_review_operation_value", @@ -515,6 +513,7 @@ def test_process_document( assert args[0] == document_processor_service.ProcessRequest() # Establish that the response is the type that we expect. + assert isinstance(response, document_processor_service.ProcessResponse) assert response.human_review_operation == "human_review_operation_value" @@ -525,19 +524,20 @@ def test_process_document_from_dict(): @pytest.mark.asyncio -async def test_process_document_async(transport: str = "grpc_asyncio"): +async def test_process_document_async( + transport: str = "grpc_asyncio", + request_type=document_processor_service.ProcessRequest, +): client = DocumentProcessorServiceAsyncClient( credentials=credentials.AnonymousCredentials(), transport=transport, ) # Everything is optional in proto3 as far as the runtime is concerned, # and we are mocking out the actual API, so just send an empty request. - request = document_processor_service.ProcessRequest() + request = request_type() # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object( - type(client._client._transport.process_document), "__call__" - ) as call: + with mock.patch.object(type(client.transport.process_document), "__call__") as call: # Designate an appropriate return value for the call. call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( document_processor_service.ProcessResponse( @@ -551,7 +551,7 @@ async def test_process_document_async(transport: str = "grpc_asyncio"): assert len(call.mock_calls) _, args, _ = call.mock_calls[0] - assert args[0] == request + assert args[0] == document_processor_service.ProcessRequest() # Establish that the response is the type that we expect. assert isinstance(response, document_processor_service.ProcessResponse) @@ -559,6 +559,11 @@ async def test_process_document_async(transport: str = "grpc_asyncio"): assert response.human_review_operation == "human_review_operation_value" +@pytest.mark.asyncio +async def test_process_document_async_from_dict(): + await test_process_document_async(request_type=dict) + + def test_process_document_field_headers(): client = DocumentProcessorServiceClient( credentials=credentials.AnonymousCredentials(), @@ -570,9 +575,7 @@ def test_process_document_field_headers(): request.name = "name/value" # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object( - type(client._transport.process_document), "__call__" - ) as call: + with mock.patch.object(type(client.transport.process_document), "__call__") as call: call.return_value = document_processor_service.ProcessResponse() client.process_document(request) @@ -599,9 +602,7 @@ async def test_process_document_field_headers_async(): request.name = "name/value" # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object( - type(client._client._transport.process_document), "__call__" - ) as call: + with mock.patch.object(type(client.transport.process_document), "__call__") as call: call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( document_processor_service.ProcessResponse() ) @@ -624,9 +625,7 @@ def test_process_document_flattened(): ) # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object( - type(client._transport.process_document), "__call__" - ) as call: + with mock.patch.object(type(client.transport.process_document), "__call__") as call: # Designate an appropriate return value for the call. call.return_value = document_processor_service.ProcessResponse() @@ -662,9 +661,7 @@ async def test_process_document_flattened_async(): ) # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object( - type(client._client._transport.process_document), "__call__" - ) as call: + with mock.patch.object(type(client.transport.process_document), "__call__") as call: # Designate an appropriate return value for the call. call.return_value = document_processor_service.ProcessResponse() @@ -710,7 +707,7 @@ def test_batch_process_documents( # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._transport.batch_process_documents), "__call__" + type(client.transport.batch_process_documents), "__call__" ) as call: # Designate an appropriate return value for the call. call.return_value = operations_pb2.Operation(name="operations/spam") @@ -732,18 +729,21 @@ def test_batch_process_documents_from_dict(): @pytest.mark.asyncio -async def test_batch_process_documents_async(transport: str = "grpc_asyncio"): +async def test_batch_process_documents_async( + transport: str = "grpc_asyncio", + request_type=document_processor_service.BatchProcessRequest, +): client = DocumentProcessorServiceAsyncClient( credentials=credentials.AnonymousCredentials(), transport=transport, ) # Everything is optional in proto3 as far as the runtime is concerned, # and we are mocking out the actual API, so just send an empty request. - request = document_processor_service.BatchProcessRequest() + request = request_type() # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client._transport.batch_process_documents), "__call__" + type(client.transport.batch_process_documents), "__call__" ) as call: # Designate an appropriate return value for the call. call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( @@ -756,12 +756,17 @@ async def test_batch_process_documents_async(transport: str = "grpc_asyncio"): assert len(call.mock_calls) _, args, _ = call.mock_calls[0] - assert args[0] == request + assert args[0] == document_processor_service.BatchProcessRequest() # Establish that the response is the type that we expect. assert isinstance(response, future.Future) +@pytest.mark.asyncio +async def test_batch_process_documents_async_from_dict(): + await test_batch_process_documents_async(request_type=dict) + + def test_batch_process_documents_field_headers(): client = DocumentProcessorServiceClient( credentials=credentials.AnonymousCredentials(), @@ -774,7 +779,7 @@ def test_batch_process_documents_field_headers(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._transport.batch_process_documents), "__call__" + type(client.transport.batch_process_documents), "__call__" ) as call: call.return_value = operations_pb2.Operation(name="operations/op") @@ -803,7 +808,7 @@ async def test_batch_process_documents_field_headers_async(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client._transport.batch_process_documents), "__call__" + type(client.transport.batch_process_documents), "__call__" ) as call: call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( operations_pb2.Operation(name="operations/op") @@ -828,7 +833,7 @@ def test_batch_process_documents_flattened(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._transport.batch_process_documents), "__call__" + type(client.transport.batch_process_documents), "__call__" ) as call: # Designate an appropriate return value for the call. call.return_value = operations_pb2.Operation(name="operations/op") @@ -866,7 +871,7 @@ async def test_batch_process_documents_flattened_async(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client._transport.batch_process_documents), "__call__" + type(client.transport.batch_process_documents), "__call__" ) as call: # Designate an appropriate return value for the call. call.return_value = operations_pb2.Operation(name="operations/op") @@ -913,7 +918,7 @@ def test_review_document( request = request_type() # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object(type(client._transport.review_document), "__call__") as call: + with mock.patch.object(type(client.transport.review_document), "__call__") as call: # Designate an appropriate return value for the call. call.return_value = operations_pb2.Operation(name="operations/spam") @@ -934,19 +939,20 @@ def test_review_document_from_dict(): @pytest.mark.asyncio -async def test_review_document_async(transport: str = "grpc_asyncio"): +async def test_review_document_async( + transport: str = "grpc_asyncio", + request_type=document_processor_service.ReviewDocumentRequest, +): client = DocumentProcessorServiceAsyncClient( credentials=credentials.AnonymousCredentials(), transport=transport, ) # Everything is optional in proto3 as far as the runtime is concerned, # and we are mocking out the actual API, so just send an empty request. - request = document_processor_service.ReviewDocumentRequest() + request = request_type() # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object( - type(client._client._transport.review_document), "__call__" - ) as call: + with mock.patch.object(type(client.transport.review_document), "__call__") as call: # Designate an appropriate return value for the call. call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( operations_pb2.Operation(name="operations/spam") @@ -958,12 +964,17 @@ async def test_review_document_async(transport: str = "grpc_asyncio"): assert len(call.mock_calls) _, args, _ = call.mock_calls[0] - assert args[0] == request + assert args[0] == document_processor_service.ReviewDocumentRequest() # Establish that the response is the type that we expect. assert isinstance(response, future.Future) +@pytest.mark.asyncio +async def test_review_document_async_from_dict(): + await test_review_document_async(request_type=dict) + + def test_review_document_field_headers(): client = DocumentProcessorServiceClient( credentials=credentials.AnonymousCredentials(), @@ -975,7 +986,7 @@ def test_review_document_field_headers(): request.human_review_config = "human_review_config/value" # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object(type(client._transport.review_document), "__call__") as call: + with mock.patch.object(type(client.transport.review_document), "__call__") as call: call.return_value = operations_pb2.Operation(name="operations/op") client.review_document(request) @@ -1005,9 +1016,7 @@ async def test_review_document_field_headers_async(): request.human_review_config = "human_review_config/value" # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object( - type(client._client._transport.review_document), "__call__" - ) as call: + with mock.patch.object(type(client.transport.review_document), "__call__") as call: call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( operations_pb2.Operation(name="operations/op") ) @@ -1033,7 +1042,7 @@ def test_review_document_flattened(): ) # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object(type(client._transport.review_document), "__call__") as call: + with mock.patch.object(type(client.transport.review_document), "__call__") as call: # Designate an appropriate return value for the call. call.return_value = operations_pb2.Operation(name="operations/op") @@ -1070,9 +1079,7 @@ async def test_review_document_flattened_async(): ) # Mock the actual call within the gRPC stub, and fake the request. - with mock.patch.object( - type(client._client._transport.review_document), "__call__" - ) as call: + with mock.patch.object(type(client.transport.review_document), "__call__") as call: # Designate an appropriate return value for the call. call.return_value = operations_pb2.Operation(name="operations/op") @@ -1144,7 +1151,7 @@ def test_transport_instance(): credentials=credentials.AnonymousCredentials(), ) client = DocumentProcessorServiceClient(transport=transport) - assert client._transport is transport + assert client.transport is transport def test_transport_get_channel(): @@ -1183,7 +1190,7 @@ def test_transport_grpc_default(): credentials=credentials.AnonymousCredentials(), ) assert isinstance( - client._transport, transports.DocumentProcessorServiceGrpcTransport, + client.transport, transports.DocumentProcessorServiceGrpcTransport, ) @@ -1285,7 +1292,7 @@ def test_document_processor_service_host_no_port(): api_endpoint="us-documentai.googleapis.com" ), ) - assert client._transport._host == "us-documentai.googleapis.com:443" + assert client.transport._host == "us-documentai.googleapis.com:443" def test_document_processor_service_host_with_port(): @@ -1295,7 +1302,7 @@ def test_document_processor_service_host_with_port(): api_endpoint="us-documentai.googleapis.com:8000" ), ) - assert client._transport._host == "us-documentai.googleapis.com:8000" + assert client.transport._host == "us-documentai.googleapis.com:8000" def test_document_processor_service_grpc_transport_channel(): @@ -1307,6 +1314,7 @@ def test_document_processor_service_grpc_transport_channel(): ) assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" + assert transport._ssl_channel_credentials == None def test_document_processor_service_grpc_asyncio_transport_channel(): @@ -1318,6 +1326,7 @@ def test_document_processor_service_grpc_asyncio_transport_channel(): ) assert transport.grpc_channel == channel assert transport._host == "squid.clam.whelk:443" + assert transport._ssl_channel_credentials == None @pytest.mark.parametrize( @@ -1365,6 +1374,7 @@ def test_document_processor_service_transport_channel_mtls_with_client_cert_sour quota_project_id=None, ) assert transport.grpc_channel == mock_grpc_channel + assert transport._ssl_channel_credentials == mock_ssl_cred @pytest.mark.parametrize( @@ -1411,7 +1421,7 @@ def test_document_processor_service_grpc_lro_client(): client = DocumentProcessorServiceClient( credentials=credentials.AnonymousCredentials(), transport="grpc", ) - transport = client._transport + transport = client.transport # Ensure that we have a api-core operations client. assert isinstance(transport.operations_client, operations_v1.OperationsClient,) @@ -1424,7 +1434,7 @@ def test_document_processor_service_grpc_lro_async_client(): client = DocumentProcessorServiceAsyncClient( credentials=credentials.AnonymousCredentials(), transport="grpc_asyncio", ) - transport = client._client._transport + transport = client.transport # Ensure that we have a api-core operations client. assert isinstance(transport.operations_client, operations_v1.OperationsAsyncClient,) @@ -1433,6 +1443,159 @@ def test_document_processor_service_grpc_lro_async_client(): assert transport.operations_client is transport.operations_client +def test_human_review_config_path(): + project = "squid" + location = "clam" + processor = "whelk" + + expected = "projects/{project}/locations/{location}/processors/{processor}/humanReviewConfig".format( + project=project, location=location, processor=processor, + ) + actual = DocumentProcessorServiceClient.human_review_config_path( + project, location, processor + ) + assert expected == actual + + +def test_parse_human_review_config_path(): + expected = { + "project": "octopus", + "location": "oyster", + "processor": "nudibranch", + } + path = DocumentProcessorServiceClient.human_review_config_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_human_review_config_path(path) + assert expected == actual + + +def test_processor_path(): + project = "cuttlefish" + location = "mussel" + processor = "winkle" + + expected = "projects/{project}/locations/{location}/processors/{processor}".format( + project=project, location=location, processor=processor, + ) + actual = DocumentProcessorServiceClient.processor_path(project, location, processor) + assert expected == actual + + +def test_parse_processor_path(): + expected = { + "project": "nautilus", + "location": "scallop", + "processor": "abalone", + } + path = DocumentProcessorServiceClient.processor_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_processor_path(path) + assert expected == actual + + +def test_common_billing_account_path(): + billing_account = "squid" + + expected = "billingAccounts/{billing_account}".format( + billing_account=billing_account, + ) + actual = DocumentProcessorServiceClient.common_billing_account_path(billing_account) + assert expected == actual + + +def test_parse_common_billing_account_path(): + expected = { + "billing_account": "clam", + } + path = DocumentProcessorServiceClient.common_billing_account_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_common_billing_account_path(path) + assert expected == actual + + +def test_common_folder_path(): + folder = "whelk" + + expected = "folders/{folder}".format(folder=folder,) + actual = DocumentProcessorServiceClient.common_folder_path(folder) + assert expected == actual + + +def test_parse_common_folder_path(): + expected = { + "folder": "octopus", + } + path = DocumentProcessorServiceClient.common_folder_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_common_folder_path(path) + assert expected == actual + + +def test_common_organization_path(): + organization = "oyster" + + expected = "organizations/{organization}".format(organization=organization,) + actual = DocumentProcessorServiceClient.common_organization_path(organization) + assert expected == actual + + +def test_parse_common_organization_path(): + expected = { + "organization": "nudibranch", + } + path = DocumentProcessorServiceClient.common_organization_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_common_organization_path(path) + assert expected == actual + + +def test_common_project_path(): + project = "cuttlefish" + + expected = "projects/{project}".format(project=project,) + actual = DocumentProcessorServiceClient.common_project_path(project) + assert expected == actual + + +def test_parse_common_project_path(): + expected = { + "project": "mussel", + } + path = DocumentProcessorServiceClient.common_project_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_common_project_path(path) + assert expected == actual + + +def test_common_location_path(): + project = "winkle" + location = "nautilus" + + expected = "projects/{project}/locations/{location}".format( + project=project, location=location, + ) + actual = DocumentProcessorServiceClient.common_location_path(project, location) + assert expected == actual + + +def test_parse_common_location_path(): + expected = { + "project": "scallop", + "location": "abalone", + } + path = DocumentProcessorServiceClient.common_location_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_common_location_path(path) + assert expected == actual + + def test_client_withDEFAULT_CLIENT_INFO(): client_info = gapic_v1.client_info.ClientInfo() From 4d3009114de6b38c29a8432fbc4850a4579ad7b7 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 18 Nov 2020 07:53:36 -0800 Subject: [PATCH 06/30] chore: update samples noxfile autosynth cannot find the source of changes triggered by earlier changes in this repository, or by version upgrades to tools such as linters. --- samples/snippets/noxfile.py | 45 ++++++++------ synth.metadata | 113 +++++++++++++++++++++++++++++++++++- 2 files changed, 139 insertions(+), 19 deletions(-) diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index 817cef92..b90eef00 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -37,22 +37,28 @@ TEST_CONFIG = { # You can opt out from the test for specific Python versions. - "ignored_versions": ["2.7"], + 'ignored_versions': ["2.7"], + + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + 'enforce_type_hints': False, + # 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)) @@ -67,13 +73,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["GCLOUD_PROJECT"] = os.environ[env_key] # deprecated + ret['GOOGLE_CLOUD_PROJECT'] = os.environ[env_key] # Apply user supplied envs. - ret.update(TEST_CONFIG["envs"]) + ret.update(TEST_CONFIG['envs']) return ret @@ -82,7 +87,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]) @@ -94,6 +99,7 @@ def get_pytest_env_vars(): def _determine_local_import_names(start_dir): """Determines all import names that should be considered "local". + This is used when running the linter to insure that import order is properly checked. """ @@ -130,17 +136,18 @@ def _determine_local_import_names(start_dir): @nox.session def lint(session): - session.install("flake8", "flake8-import-order") + if not TEST_CONFIG['enforce_type_hints']: + session.install("flake8", "flake8-import-order") + else: + session.install("flake8", "flake8-import-order", "flake8-annotations") local_names = _determine_local_import_names(".") args = FLAKE8_COMMON_ARGS + [ "--application-import-names", ",".join(local_names), - ".", + "." ] session.run("flake8", *args) - - # # Black # @@ -153,7 +160,6 @@ def blacken(session): session.run("black", *python_files) - # # Sample Tests # @@ -193,9 +199,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 + )) # @@ -212,6 +218,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/synth.metadata b/synth.metadata index 8088f01c..22548ac5 100644 --- a/synth.metadata +++ b/synth.metadata @@ -3,8 +3,8 @@ { "git": { "name": ".", - "remote": "git@github.com:googleapis/python-documentai", - "sha": "c6186ea7a58f83bc2e49d9df2a48fce3f78f0143" + "remote": "https://github.com/googleapis/python-documentai.git", + "sha": "4918e62033b4c118bf99ba83730377b4ecc86d17" } }, { @@ -49,5 +49,114 @@ "generator": "bazel" } } + ], + "generatedFiles": [ + ".flake8", + ".github/CONTRIBUTING.md", + ".github/ISSUE_TEMPLATE/bug_report.md", + ".github/ISSUE_TEMPLATE/feature_request.md", + ".github/ISSUE_TEMPLATE/support_request.md", + ".github/PULL_REQUEST_TEMPLATE.md", + ".github/release-please.yml", + ".github/snippet-bot.yml", + ".gitignore", + ".kokoro/build.sh", + ".kokoro/continuous/common.cfg", + ".kokoro/continuous/continuous.cfg", + ".kokoro/docker/docs/Dockerfile", + ".kokoro/docker/docs/fetch_gpg_keys.sh", + ".kokoro/docs/common.cfg", + ".kokoro/docs/docs-presubmit.cfg", + ".kokoro/docs/docs.cfg", + ".kokoro/populate-secrets.sh", + ".kokoro/presubmit/common.cfg", + ".kokoro/presubmit/presubmit.cfg", + ".kokoro/publish-docs.sh", + ".kokoro/release.sh", + ".kokoro/release/common.cfg", + ".kokoro/release/release.cfg", + ".kokoro/samples/lint/common.cfg", + ".kokoro/samples/lint/continuous.cfg", + ".kokoro/samples/lint/periodic.cfg", + ".kokoro/samples/lint/presubmit.cfg", + ".kokoro/samples/python3.6/common.cfg", + ".kokoro/samples/python3.6/continuous.cfg", + ".kokoro/samples/python3.6/periodic.cfg", + ".kokoro/samples/python3.6/presubmit.cfg", + ".kokoro/samples/python3.7/common.cfg", + ".kokoro/samples/python3.7/continuous.cfg", + ".kokoro/samples/python3.7/periodic.cfg", + ".kokoro/samples/python3.7/presubmit.cfg", + ".kokoro/samples/python3.8/common.cfg", + ".kokoro/samples/python3.8/continuous.cfg", + ".kokoro/samples/python3.8/periodic.cfg", + ".kokoro/samples/python3.8/presubmit.cfg", + ".kokoro/test-samples.sh", + ".kokoro/trampoline.sh", + ".kokoro/trampoline_v2.sh", + ".trampolinerc", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.rst", + "LICENSE", + "MANIFEST.in", + "docs/_static/custom.css", + "docs/_templates/layout.html", + "docs/conf.py", + "docs/documentai_v1beta2/services.rst", + "docs/documentai_v1beta2/types.rst", + "docs/documentai_v1beta3/services.rst", + "docs/documentai_v1beta3/types.rst", + "docs/multiprocessing.rst", + "google/cloud/documentai/__init__.py", + "google/cloud/documentai/py.typed", + "google/cloud/documentai_v1beta2/__init__.py", + "google/cloud/documentai_v1beta2/py.typed", + "google/cloud/documentai_v1beta2/services/__init__.py", + "google/cloud/documentai_v1beta2/services/document_understanding_service/__init__.py", + "google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py", + "google/cloud/documentai_v1beta2/services/document_understanding_service/client.py", + "google/cloud/documentai_v1beta2/services/document_understanding_service/transports/__init__.py", + "google/cloud/documentai_v1beta2/services/document_understanding_service/transports/base.py", + "google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py", + "google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py", + "google/cloud/documentai_v1beta2/types/__init__.py", + "google/cloud/documentai_v1beta2/types/document.py", + "google/cloud/documentai_v1beta2/types/document_understanding.py", + "google/cloud/documentai_v1beta2/types/geometry.py", + "google/cloud/documentai_v1beta3/__init__.py", + "google/cloud/documentai_v1beta3/py.typed", + "google/cloud/documentai_v1beta3/services/__init__.py", + "google/cloud/documentai_v1beta3/services/document_processor_service/__init__.py", + "google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py", + "google/cloud/documentai_v1beta3/services/document_processor_service/client.py", + "google/cloud/documentai_v1beta3/services/document_processor_service/transports/__init__.py", + "google/cloud/documentai_v1beta3/services/document_processor_service/transports/base.py", + "google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py", + "google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py", + "google/cloud/documentai_v1beta3/types/__init__.py", + "google/cloud/documentai_v1beta3/types/document.py", + "google/cloud/documentai_v1beta3/types/document_processor_service.py", + "google/cloud/documentai_v1beta3/types/geometry.py", + "mypy.ini", + "noxfile.py", + "renovate.json", + "samples/AUTHORING_GUIDE.md", + "samples/CONTRIBUTING.md", + "samples/snippets/noxfile.py", + "scripts/decrypt-secrets.sh", + "scripts/fixup_documentai_v1beta2_keywords.py", + "scripts/fixup_documentai_v1beta3_keywords.py", + "scripts/readme-gen/readme_gen.py", + "scripts/readme-gen/templates/README.tmpl.rst", + "scripts/readme-gen/templates/auth.tmpl.rst", + "scripts/readme-gen/templates/auth_api_key.tmpl.rst", + "scripts/readme-gen/templates/install_deps.tmpl.rst", + "scripts/readme-gen/templates/install_portaudio.tmpl.rst", + "setup.cfg", + "testing/.gitignore", + "tests/unit/gapic/documentai_v1beta2/__init__.py", + "tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py", + "tests/unit/gapic/documentai_v1beta3/__init__.py", + "tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py" ] } \ No newline at end of file From 1b24bfdfc603952db8d1c633dfde108a396aa707 Mon Sep 17 00:00:00 2001 From: Eric Schmidt Date: Fri, 20 Nov 2020 10:39:35 -0800 Subject: [PATCH 07/30] fix: removes C-style semicolons and slash comments (#59) --- samples/snippets/process_document_sample_v1beta3.py | 8 ++++---- samples/snippets/quickstart_sample_v1beta3.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/samples/snippets/process_document_sample_v1beta3.py b/samples/snippets/process_document_sample_v1beta3.py index e666affa..29e66b19 100644 --- a/samples/snippets/process_document_sample_v1beta3.py +++ b/samples/snippets/process_document_sample_v1beta3.py @@ -18,10 +18,10 @@ # [START documentai_process_document] # TODO(developer): Uncomment these variables before running the sample. -# project_id= 'YOUR_PROJECT_ID'; -# location = 'YOUR_PROJECT_LOCATION'; // Format is 'us' or 'eu' -# processor_id = 'YOUR_PROCESSOR_ID'; // Create processor in Cloud Console -# file_path = '/path/to/local/pdf'; +# project_id= 'YOUR_PROJECT_ID' +# location = 'YOUR_PROJECT_LOCATION' # Format is 'us' or 'eu' +# processor_id = 'YOUR_PROCESSOR_ID' # Create processor in Cloud Console +# file_path = '/path/to/local/pdf' def process_document_sample( diff --git a/samples/snippets/quickstart_sample_v1beta3.py b/samples/snippets/quickstart_sample_v1beta3.py index 7eb4a8ff..37d44bb0 100644 --- a/samples/snippets/quickstart_sample_v1beta3.py +++ b/samples/snippets/quickstart_sample_v1beta3.py @@ -18,10 +18,10 @@ # [START documentai_quickstart] # TODO(developer): Uncomment these variables before running the sample. -# project_id= 'YOUR_PROJECT_ID'; -# location = 'YOUR_PROJECT_LOCATION'; # Format is 'us' or 'eu' -# processor_id = 'YOUR_PROCESSOR_ID'; # Create processor in Cloud Console -# file_path = '/path/to/local/pdf'; +# project_id= 'YOUR_PROJECT_ID' +# location = 'YOUR_PROJECT_LOCATION' # Format is 'us' or 'eu' +# processor_id = 'YOUR_PROCESSOR_ID' # Create processor in Cloud Console +# file_path = '/path/to/local/pdf' def quickstart(project_id: str, location: str, processor_id: str, file_path: str): From 1a2bcc26a4aeefc96560fc9170977947f0bc9116 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 25 Nov 2020 20:30:45 +0100 Subject: [PATCH 08/30] chore(deps): update dependency google-cloud-storage to v1.33.0 (#61) --- 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 fbe576b0..b0259adc 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,2 +1,2 @@ google-cloud-documentai==0.3.0 -google-cloud-storage==1.32.0 +google-cloud-storage==1.33.0 From 7f7f541bcf4d2f42b2f619c2ceb45f53c5d0e9eb Mon Sep 17 00:00:00 2001 From: Mike <45373284+munkhuushmgl@users.noreply.github.com> Date: Wed, 2 Dec 2020 14:26:12 -0800 Subject: [PATCH 09/30] fix: added if statement to filter out dir blob files (#63) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #62 🦕 Current version of sample doesnt check if blob is directory or .json file. Then, it downloads as bytes and tries to parse json from the dir blob file which will cause error. --- samples/snippets/batch_process_documents_sample_v1beta3.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/samples/snippets/batch_process_documents_sample_v1beta3.py b/samples/snippets/batch_process_documents_sample_v1beta3.py index 6e22e0ea..ea6c01e3 100644 --- a/samples/snippets/batch_process_documents_sample_v1beta3.py +++ b/samples/snippets/batch_process_documents_sample_v1beta3.py @@ -78,9 +78,12 @@ def batch_process_documents( for i, blob in enumerate(blob_list): # Download the contents of this blob as a bytes object. + if ".json" not in blob.name: + return + # Only parses JSON files blob_as_bytes = blob.download_as_bytes() - document = documentai.types.Document.from_json(blob_as_bytes) + document = documentai.types.Document.from_json(blob_as_bytes) print(f"Fetched file {i + 1}") # For a full list of Document object attributes, please reference this page: https://googleapis.dev/python/documentai/latest/_modules/google/cloud/documentai_v1beta3/types/document.html#Document From bf3aba33bebba4f6893cee00c595d3b6ed333a3b Mon Sep 17 00:00:00 2001 From: Eric Schmidt Date: Thu, 3 Dec 2020 16:10:55 -0800 Subject: [PATCH 10/30] samples(fix): change comments to match function signature (#68) --- samples/snippets/batch_process_documents_sample_v1beta3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/batch_process_documents_sample_v1beta3.py b/samples/snippets/batch_process_documents_sample_v1beta3.py index ea6c01e3..dcedbbf5 100644 --- a/samples/snippets/batch_process_documents_sample_v1beta3.py +++ b/samples/snippets/batch_process_documents_sample_v1beta3.py @@ -23,7 +23,7 @@ # project_id= 'YOUR_PROJECT_ID' # location = 'YOUR_PROJECT_LOCATION' # Format is 'us' or 'eu' # processor_id = 'YOUR_PROCESSOR_ID' # Create processor in Cloud Console -# input_uri = "YOUR_INPUT_URI" +# gcs_input_uri = "YOUR_INPUT_URI" # gcs_output_uri = "YOUR_OUTPUT_BUCKET_URI" # gcs_output_uri_prefix = "YOUR_OUTPUT_URI_PREFIX" From a04fbeaf026d3d204dbb6c6cecf181068ddcc882 Mon Sep 17 00:00:00 2001 From: Eric Schmidt Date: Wed, 9 Dec 2020 09:44:14 -0800 Subject: [PATCH 11/30] fix: moves import statment inside region tags (#71) --- samples/snippets/process_document_sample_v1beta3.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/snippets/process_document_sample_v1beta3.py b/samples/snippets/process_document_sample_v1beta3.py index 29e66b19..5b045708 100644 --- a/samples/snippets/process_document_sample_v1beta3.py +++ b/samples/snippets/process_document_sample_v1beta3.py @@ -13,8 +13,6 @@ # limitations under the License. # -from google.cloud import documentai_v1beta3 as documentai - # [START documentai_process_document] # TODO(developer): Uncomment these variables before running the sample. @@ -27,6 +25,8 @@ def process_document_sample( project_id: str, location: str, processor_id: str, file_path: str ): + from google.cloud import documentai_v1beta3 as documentai + # Instantiates a client client = documentai.DocumentProcessorServiceClient() From d1222a796ee71bcdf198660d9742fdaa5f9b0fc9 Mon Sep 17 00:00:00 2001 From: Mike <45373284+munkhuushmgl@users.noreply.github.com> Date: Fri, 11 Dec 2020 11:27:00 -0800 Subject: [PATCH 12/30] samples: added test that covers the wrong file type case (#69) * samples: added test that covers the wrong file type case --- ...documents_sample_bad_input_v1beta3_test.py | 44 +++++++++++++++++++ .../batch_process_documents_sample_v1beta3.py | 4 +- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 samples/snippets/batch_process_documents_sample_bad_input_v1beta3_test.py diff --git a/samples/snippets/batch_process_documents_sample_bad_input_v1beta3_test.py b/samples/snippets/batch_process_documents_sample_bad_input_v1beta3_test.py new file mode 100644 index 00000000..e0a7e468 --- /dev/null +++ b/samples/snippets/batch_process_documents_sample_bad_input_v1beta3_test.py @@ -0,0 +1,44 @@ +# 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 +from uuid import uuid4 + +from samples.snippets import batch_process_documents_sample_v1beta3 + +location = "us" +project_id = os.getenv("GOOGLE_CLOUD_PROJECT") +processor_id = "90484cfdedb024f6" +gcs_input_uri = "gs://cloud-samples-data/documentai/invoice.pdf" +# following bucket contains .csv file which will cause the sample to fail. +gcs_output_full_uri_with_wrong_type = "gs://documentai-beta-samples" +BUCKET_NAME = f"document-ai-python-{uuid4()}" + + +def test_batch_process_documents_with_bad_input(capsys): + try: + batch_process_documents_sample_v1beta3.batch_process_documents( + project_id=project_id, + location=location, + processor_id=processor_id, + gcs_input_uri=gcs_input_uri, + gcs_output_uri=gcs_output_full_uri_with_wrong_type, + gcs_output_uri_prefix="test", + timeout=450, + ) + out, _ = capsys.readouterr() + assert "Failed to process" in out + except Exception as e: + assert "Failed to process" in e.message diff --git a/samples/snippets/batch_process_documents_sample_v1beta3.py b/samples/snippets/batch_process_documents_sample_v1beta3.py index dcedbbf5..dae938b2 100644 --- a/samples/snippets/batch_process_documents_sample_v1beta3.py +++ b/samples/snippets/batch_process_documents_sample_v1beta3.py @@ -35,6 +35,7 @@ def batch_process_documents( gcs_input_uri, gcs_output_uri, gcs_output_uri_prefix, + timeout: int = 300, ): client = documentai.DocumentProcessorServiceClient() @@ -63,7 +64,7 @@ def batch_process_documents( operation = client.batch_process_documents(request) # Wait for the operation to finish - operation.result() + operation.result(timeout=timeout) # Results are written to GCS. Use a regex to find # output files @@ -79,6 +80,7 @@ def batch_process_documents( for i, blob in enumerate(blob_list): # Download the contents of this blob as a bytes object. if ".json" not in blob.name: + print(f"skipping non-supported file type {blob.name}") return # Only parses JSON files blob_as_bytes = blob.download_as_bytes() From c94afd55124b0abc8978bf86b84743dd4afb0778 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Mon, 28 Dec 2020 10:11:46 -0800 Subject: [PATCH 13/30] fix: remove client recv msg limit and add enums to `types/__init__.py` (#72) PiperOrigin-RevId: 347055288 Source-Author: Google APIs Source-Date: Fri Dec 11 12:44:37 2020 -0800 Source-Repo: googleapis/googleapis Source-Sha: dd372aa22ded7a8ba6f0e03a80e06358a3fa0907 Source-Link: https://github.com/googleapis/googleapis/commit/dd372aa22ded7a8ba6f0e03a80e06358a3fa0907 --- .../transports/__init__.py | 1 - .../transports/grpc.py | 19 +++++++++++++------ .../transports/grpc_asyncio.py | 15 ++++++++++++--- .../documentai_v1beta2/types/__init__.py | 1 - .../transports/__init__.py | 1 - .../transports/grpc.py | 19 +++++++++++++------ .../transports/grpc_asyncio.py | 15 ++++++++++++--- .../documentai_v1beta3/types/__init__.py | 1 - synth.metadata | 6 +++--- .../test_document_understanding_service.py | 8 ++++++++ .../test_document_processor_service.py | 8 ++++++++ 11 files changed, 69 insertions(+), 25 deletions(-) diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/__init__.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/__init__.py index ce42f2ab..d296b9d5 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/__init__.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/__init__.py @@ -30,7 +30,6 @@ _transport_registry["grpc"] = DocumentUnderstandingServiceGrpcTransport _transport_registry["grpc_asyncio"] = DocumentUnderstandingServiceGrpcAsyncIOTransport - __all__ = ( "DocumentUnderstandingServiceTransport", "DocumentUnderstandingServiceGrpcTransport", diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py index 0390ff51..230d39f2 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py @@ -151,6 +151,10 @@ def __init__( ssl_credentials=ssl_credentials, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], ) self._ssl_channel_credentials = ssl_credentials else: @@ -169,9 +173,14 @@ def __init__( ssl_credentials=ssl_channel_credentials, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], ) self._stubs = {} # type: Dict[str, Callable] + self._operations_client = None # Run the base constructor. super().__init__( @@ -195,7 +204,7 @@ def create_channel( ) -> grpc.Channel: """Create and return a gRPC channel object. Args: - address (Optionsl[str]): The host for the channel to use. + address (Optional[str]): The host for the channel to use. credentials (Optional[~.Credentials]): The authorization credentials to attach to requests. These credentials identify this application to the service. If @@ -242,13 +251,11 @@ def operations_client(self) -> operations_v1.OperationsClient: client. """ # Sanity check: Only create a new client if we do not already have one. - if "operations_client" not in self.__dict__: - self.__dict__["operations_client"] = operations_v1.OperationsClient( - self.grpc_channel - ) + if self._operations_client is None: + self._operations_client = operations_v1.OperationsClient(self.grpc_channel) # Return the client from cache. - return self.__dict__["operations_client"] + return self._operations_client @property def batch_process_documents( diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py index 95122515..7e78a8cd 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py @@ -198,6 +198,10 @@ def __init__( ssl_credentials=ssl_credentials, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], ) self._ssl_channel_credentials = ssl_credentials else: @@ -216,6 +220,10 @@ def __init__( ssl_credentials=ssl_channel_credentials, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], ) # Run the base constructor. @@ -229,6 +237,7 @@ def __init__( ) self._stubs = {} + self._operations_client = None @property def grpc_channel(self) -> aio.Channel: @@ -248,13 +257,13 @@ def operations_client(self) -> operations_v1.OperationsAsyncClient: client. """ # Sanity check: Only create a new client if we do not already have one. - if "operations_client" not in self.__dict__: - self.__dict__["operations_client"] = operations_v1.OperationsAsyncClient( + if self._operations_client is None: + self._operations_client = operations_v1.OperationsAsyncClient( self.grpc_channel ) # Return the client from cache. - return self.__dict__["operations_client"] + return self._operations_client @property def batch_process_documents( diff --git a/google/cloud/documentai_v1beta2/types/__init__.py b/google/cloud/documentai_v1beta2/types/__init__.py index 5d05c6b1..73b83af3 100644 --- a/google/cloud/documentai_v1beta2/types/__init__.py +++ b/google/cloud/documentai_v1beta2/types/__init__.py @@ -40,7 +40,6 @@ OperationMetadata, ) - __all__ = ( "Vertex", "NormalizedVertex", diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/__init__.py b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/__init__.py index a613297c..e3e820b3 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/__init__.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/__init__.py @@ -30,7 +30,6 @@ _transport_registry["grpc"] = DocumentProcessorServiceGrpcTransport _transport_registry["grpc_asyncio"] = DocumentProcessorServiceGrpcAsyncIOTransport - __all__ = ( "DocumentProcessorServiceTransport", "DocumentProcessorServiceGrpcTransport", diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py index 435767b0..32bf1e00 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py @@ -152,6 +152,10 @@ def __init__( ssl_credentials=ssl_credentials, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], ) self._ssl_channel_credentials = ssl_credentials else: @@ -170,9 +174,14 @@ def __init__( ssl_credentials=ssl_channel_credentials, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], ) self._stubs = {} # type: Dict[str, Callable] + self._operations_client = None # Run the base constructor. super().__init__( @@ -196,7 +205,7 @@ def create_channel( ) -> grpc.Channel: """Create and return a gRPC channel object. Args: - address (Optionsl[str]): The host for the channel to use. + address (Optional[str]): The host for the channel to use. credentials (Optional[~.Credentials]): The authorization credentials to attach to requests. These credentials identify this application to the service. If @@ -243,13 +252,11 @@ def operations_client(self) -> operations_v1.OperationsClient: client. """ # Sanity check: Only create a new client if we do not already have one. - if "operations_client" not in self.__dict__: - self.__dict__["operations_client"] = operations_v1.OperationsClient( - self.grpc_channel - ) + if self._operations_client is None: + self._operations_client = operations_v1.OperationsClient(self.grpc_channel) # Return the client from cache. - return self.__dict__["operations_client"] + return self._operations_client @property def process_document( diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py index df9be8a8..5e3b676d 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py @@ -197,6 +197,10 @@ def __init__( ssl_credentials=ssl_credentials, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], ) self._ssl_channel_credentials = ssl_credentials else: @@ -215,6 +219,10 @@ def __init__( ssl_credentials=ssl_channel_credentials, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], ) # Run the base constructor. @@ -228,6 +236,7 @@ def __init__( ) self._stubs = {} + self._operations_client = None @property def grpc_channel(self) -> aio.Channel: @@ -247,13 +256,13 @@ def operations_client(self) -> operations_v1.OperationsAsyncClient: client. """ # Sanity check: Only create a new client if we do not already have one. - if "operations_client" not in self.__dict__: - self.__dict__["operations_client"] = operations_v1.OperationsAsyncClient( + if self._operations_client is None: + self._operations_client = operations_v1.OperationsAsyncClient( self.grpc_channel ) # Return the client from cache. - return self.__dict__["operations_client"] + return self._operations_client @property def process_document( diff --git a/google/cloud/documentai_v1beta3/types/__init__.py b/google/cloud/documentai_v1beta3/types/__init__.py index 4b5768f7..3c34fd3b 100644 --- a/google/cloud/documentai_v1beta3/types/__init__.py +++ b/google/cloud/documentai_v1beta3/types/__init__.py @@ -32,7 +32,6 @@ ReviewDocumentOperationMetadata, ) - __all__ = ( "Vertex", "NormalizedVertex", diff --git a/synth.metadata b/synth.metadata index 22548ac5..fd52e0cd 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,15 +4,15 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/python-documentai.git", - "sha": "4918e62033b4c118bf99ba83730377b4ecc86d17" + "sha": "d1222a796ee71bcdf198660d9742fdaa5f9b0fc9" } }, { "git": { "name": "googleapis", "remote": "https://github.com/googleapis/googleapis.git", - "sha": "e3e7e7ddb0fecd7bc61ca03b5a9ddb29cc9b48d8", - "internalRef": "342967619" + "sha": "dd372aa22ded7a8ba6f0e03a80e06358a3fa0907", + "internalRef": "347055288" } }, { diff --git a/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py b/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py index 17fdb324..507a10de 100644 --- a/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py +++ b/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py @@ -1098,6 +1098,10 @@ def test_document_understanding_service_transport_channel_mtls_with_client_cert_ scopes=("https://www.googleapis.com/auth/cloud-platform",), ssl_credentials=mock_ssl_cred, quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], ) assert transport.grpc_channel == mock_grpc_channel assert transport._ssl_channel_credentials == mock_ssl_cred @@ -1141,6 +1145,10 @@ def test_document_understanding_service_transport_channel_mtls_with_adc( scopes=("https://www.googleapis.com/auth/cloud-platform",), ssl_credentials=mock_ssl_cred, quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], ) assert transport.grpc_channel == mock_grpc_channel diff --git a/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py b/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py index ad4346e8..bfe8ffe9 100644 --- a/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py +++ b/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py @@ -1372,6 +1372,10 @@ def test_document_processor_service_transport_channel_mtls_with_client_cert_sour scopes=("https://www.googleapis.com/auth/cloud-platform",), ssl_credentials=mock_ssl_cred, quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], ) assert transport.grpc_channel == mock_grpc_channel assert transport._ssl_channel_credentials == mock_ssl_cred @@ -1413,6 +1417,10 @@ def test_document_processor_service_transport_channel_mtls_with_adc(transport_cl scopes=("https://www.googleapis.com/auth/cloud-platform",), ssl_credentials=mock_ssl_cred, quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], ) assert transport.grpc_channel == mock_grpc_channel From 33dc25806d5afd147c7cfb4b5f9c5505683b7ec4 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 29 Dec 2020 08:14:52 -0800 Subject: [PATCH 14/30] chore: update templates (#74) * docs(python): update intersphinx for grpc and auth * docs(python): update intersphinx for grpc and auth * use https for python intersphinx Co-authored-by: Tim Swast Source-Author: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Source-Date: Wed Nov 18 14:37:25 2020 -0700 Source-Repo: googleapis/synthtool Source-Sha: 9a7d9fbb7045c34c9d3d22c1ff766eeae51f04c9 Source-Link: https://github.com/googleapis/synthtool/commit/9a7d9fbb7045c34c9d3d22c1ff766eeae51f04c9 * docs(python): fix intersphinx link for google-auth Source-Author: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Source-Date: Thu Nov 19 10:16:05 2020 -0700 Source-Repo: googleapis/synthtool Source-Sha: a073c873f3928c561bdf87fdfbf1d081d1998984 Source-Link: https://github.com/googleapis/synthtool/commit/a073c873f3928c561bdf87fdfbf1d081d1998984 * chore: add config / docs for 'pre-commit' support Source-Author: Tres Seaver Source-Date: Tue Dec 1 16:01:20 2020 -0500 Source-Repo: googleapis/synthtool Source-Sha: 32af6da519a6b042e3da62008e2a75e991efb6b4 Source-Link: https://github.com/googleapis/synthtool/commit/32af6da519a6b042e3da62008e2a75e991efb6b4 * chore(deps): update precommit hook pre-commit/pre-commit-hooks to v3.3.0 Source-Author: WhiteSource Renovate Source-Date: Wed Dec 2 17:18:24 2020 +0100 Source-Repo: googleapis/synthtool Source-Sha: 69629b64b83c6421d616be2b8e11795738ec8a6c Source-Link: https://github.com/googleapis/synthtool/commit/69629b64b83c6421d616be2b8e11795738ec8a6c * test(python): give filesystem paths to pytest-cov https://pytest-cov.readthedocs.io/en/latest/config.html The pytest-cov docs seem to suggest a filesystem path is expected. Source-Author: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Source-Date: Wed Dec 2 09:28:04 2020 -0700 Source-Repo: googleapis/synthtool Source-Sha: f94318521f63085b9ccb43d42af89f153fb39f15 Source-Link: https://github.com/googleapis/synthtool/commit/f94318521f63085b9ccb43d42af89f153fb39f15 * chore: update noxfile.py.j2 * Update noxfile.py.j2 add changes from @glasnt to the template template to ensure that enforcing type hinting doesn't fail for repos with the sample noxfile (aka all samples repos) See https://github.com/GoogleCloudPlatform/python-docs-samples/pull/4869/files for context * fix typo Source-Author: Leah E. Cole <6719667+leahecole@users.noreply.github.com> Source-Date: Thu Dec 3 13:44:30 2020 -0800 Source-Repo: googleapis/synthtool Source-Sha: 18c5dbdb4ac8cf75d4d8174e7b4558f48e76f8a1 Source-Link: https://github.com/googleapis/synthtool/commit/18c5dbdb4ac8cf75d4d8174e7b4558f48e76f8a1 * chore(deps): update precommit hook pre-commit/pre-commit-hooks to v3.4.0 Co-authored-by: Tres Seaver Source-Author: WhiteSource Renovate Source-Date: Wed Dec 16 18:13:24 2020 +0100 Source-Repo: googleapis/synthtool Source-Sha: aa255b15d52b6d8950cca48cfdf58f7d27a60c8a Source-Link: https://github.com/googleapis/synthtool/commit/aa255b15d52b6d8950cca48cfdf58f7d27a60c8a * docs(python): document adding Python 3.9 support, dropping 3.5 support Closes #787 Source-Author: Tres Seaver Source-Date: Thu Dec 17 16:08:02 2020 -0500 Source-Repo: googleapis/synthtool Source-Sha: b670a77a454f415d247907908e8ee7943e06d718 Source-Link: https://github.com/googleapis/synthtool/commit/b670a77a454f415d247907908e8ee7943e06d718 * chore: exclude `.nox` directories from linting The samples tests create `.nox` directories with all dependencies installed. These directories should be excluded from linting. I've tested this change locally, and it significantly speeds up linting on my machine. Source-Author: Tim Swast Source-Date: Tue Dec 22 13:04:04 2020 -0600 Source-Repo: googleapis/synthtool Source-Sha: 373861061648b5fe5e0ac4f8a38b32d639ee93e4 Source-Link: https://github.com/googleapis/synthtool/commit/373861061648b5fe5e0ac4f8a38b32d639ee93e4 --- .flake8 | 1 + .pre-commit-config.yaml | 17 +++++++++++++++++ CONTRIBUTING.rst | 21 +++++++++++++++------ docs/conf.py | 6 +++--- noxfile.py | 5 ++--- samples/snippets/noxfile.py | 17 +++++++++-------- synth.metadata | 7 ++++--- 7 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.flake8 b/.flake8 index ed931638..29227d4c 100644 --- a/.flake8 +++ b/.flake8 @@ -26,6 +26,7 @@ exclude = *_pb2.py # Standard linting exemptions. + **/.nox/** __pycache__, .git, *.pyc, diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..a9024b15 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml +- repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 + hooks: + - id: flake8 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 9ea187b2..e7cc1eaf 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -21,8 +21,8 @@ In order to add a feature: - The feature must be documented in both the API and narrative documentation. -- The feature must work fully on the following CPython versions: 2.7, - 3.5, 3.6, 3.7 and 3.8 on both UNIX and Windows. +- The feature must work fully on the following CPython versions: + 3.6, 3.7, 3.8 and 3.9 on both UNIX and Windows. - The feature must not add unnecessary dependencies (where "unnecessary" is of course subjective, but new dependencies should @@ -111,6 +111,16 @@ Coding Style should point to the official ``googleapis`` checkout and the the branch should be the main branch on that remote (``master``). +- This repository contains configuration for the + `pre-commit `__ tool, which automates checking + our linters during a commit. If you have it installed on your ``$PATH``, + you can enable enforcing those checks via: + +.. code-block:: bash + + $ pre-commit install + pre-commit installed at .git/hooks/pre-commit + Exceptions to PEP8: - Many unit tests use a helper method, ``_call_fut`` ("FUT" is short for @@ -192,25 +202,24 @@ Supported Python Versions We support: -- `Python 3.5`_ - `Python 3.6`_ - `Python 3.7`_ - `Python 3.8`_ +- `Python 3.9`_ -.. _Python 3.5: https://docs.python.org/3.5/ .. _Python 3.6: https://docs.python.org/3.6/ .. _Python 3.7: https://docs.python.org/3.7/ .. _Python 3.8: https://docs.python.org/3.8/ +.. _Python 3.9: https://docs.python.org/3.9/ Supported versions can be found in our ``noxfile.py`` `config`_. .. _config: https://github.com/googleapis/python-documentai/blob/master/noxfile.py -Python 2.7 support is deprecated. All code changes should maintain Python 2.7 compatibility until January 1, 2020. We also explicitly decided to support Python 3 beginning with version -3.5. Reasons for this include: +3.6. Reasons for this include: - Encouraging use of newest versions of Python 3 - Taking the lead of `prominent`_ open-source `projects`_ diff --git a/docs/conf.py b/docs/conf.py index 6d80d2bf..4982dd92 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -345,10 +345,10 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - "python": ("http://python.readthedocs.org/en/latest/", None), - "google-auth": ("https://google-auth.readthedocs.io/en/stable", None), + "python": ("https://python.readthedocs.org/en/latest/", None), + "google-auth": ("https://googleapis.dev/python/google-auth/latest/", None), "google.api_core": ("https://googleapis.dev/python/google-api-core/latest/", None,), - "grpc": ("https://grpc.io/grpc/python/", None), + "grpc": ("https://grpc.github.io/grpc/python/", None), "proto-plus": ("https://proto-plus-python.readthedocs.io/en/latest/", None), } diff --git a/noxfile.py b/noxfile.py index d531ac69..8004482e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -81,9 +81,8 @@ def default(session): session.run( "py.test", "--quiet", - "--cov=google.cloud.documentai", - "--cov=google.cloud", - "--cov=tests.unit", + "--cov=google/cloud", + "--cov=tests/unit", "--cov-append", "--cov-config=.coveragerc", "--cov-report=", diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index b90eef00..bca0522e 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -17,6 +17,7 @@ import os from pathlib import Path import sys +from typing import Callable, Dict, List, Optional import nox @@ -68,7 +69,7 @@ TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) -def get_pytest_env_vars(): +def get_pytest_env_vars() -> Dict[str, str]: """Returns a dict for pytest invocation.""" ret = {} @@ -97,7 +98,7 @@ def get_pytest_env_vars(): # -def _determine_local_import_names(start_dir): +def _determine_local_import_names(start_dir: str) -> List[str]: """Determines all import names that should be considered "local". This is used when running the linter to insure that import order is @@ -135,7 +136,7 @@ def _determine_local_import_names(start_dir): @nox.session -def lint(session): +def lint(session: nox.sessions.Session) -> None: if not TEST_CONFIG['enforce_type_hints']: session.install("flake8", "flake8-import-order") else: @@ -154,7 +155,7 @@ def lint(session): @nox.session -def blacken(session): +def blacken(session: nox.sessions.Session) -> None: session.install("black") python_files = [path for path in os.listdir(".") if path.endswith(".py")] @@ -168,7 +169,7 @@ def blacken(session): PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] -def _session_tests(session, post_install=None): +def _session_tests(session: nox.sessions.Session, post_install: Callable = None) -> None: """Runs py.test for a particular project.""" if os.path.exists("requirements.txt"): session.install("-r", "requirements.txt") @@ -194,7 +195,7 @@ def _session_tests(session, post_install=None): @nox.session(python=ALL_VERSIONS) -def py(session): +def py(session: nox.sessions.Session) -> None: """Runs py.test for a sample using the specified version of Python.""" if session.python in TESTED_VERSIONS: _session_tests(session) @@ -209,7 +210,7 @@ def py(session): # -def _get_repo_root(): +def _get_repo_root() -> Optional[str]: """ Returns the root folder of the project. """ # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) @@ -232,7 +233,7 @@ def _get_repo_root(): @nox.session @nox.parametrize("path", GENERATED_READMES) -def readmegen(session, path): +def readmegen(session: nox.sessions.Session, path: str) -> None: """(Re-)generates the readme for a sample.""" session.install("jinja2", "pyyaml") dir_ = os.path.dirname(path) diff --git a/synth.metadata b/synth.metadata index fd52e0cd..8d9424c7 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,7 +4,7 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/python-documentai.git", - "sha": "d1222a796ee71bcdf198660d9742fdaa5f9b0fc9" + "sha": "c94afd55124b0abc8978bf86b84743dd4afb0778" } }, { @@ -19,14 +19,14 @@ "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "7fcc405a579d5d53a726ff3da1b7c8c08f0f2d58" + "sha": "373861061648b5fe5e0ac4f8a38b32d639ee93e4" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "7fcc405a579d5d53a726ff3da1b7c8c08f0f2d58" + "sha": "373861061648b5fe5e0ac4f8a38b32d639ee93e4" } } ], @@ -94,6 +94,7 @@ ".kokoro/test-samples.sh", ".kokoro/trampoline.sh", ".kokoro/trampoline_v2.sh", + ".pre-commit-config.yaml", ".trampolinerc", "CODE_OF_CONDUCT.md", "CONTRIBUTING.rst", From d6f183a696b211c6d29bc28e9bbd0a8537f65577 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Wed, 6 Jan 2021 08:51:54 -0800 Subject: [PATCH 15/30] feat: add from_service_account_info factory and fix sphinx identifiers (#80) feat: add 'from_service_account_info' factory to clients fix: fix sphinx identifiers PiperOrigin-RevId: 350246057 Source-Author: Google APIs Source-Date: Tue Jan 5 16:44:11 2021 -0800 Source-Repo: googleapis/googleapis Source-Sha: 520682435235d9c503983a360a2090025aa47cd1 Source-Link: https://github.com/googleapis/googleapis/commit/520682435235d9c503983a360a2090025aa47cd1 --- .coveragerc | 2 +- .../document_understanding_service.rst | 6 + docs/documentai_v1beta2/services.rst | 6 +- docs/documentai_v1beta2/types.rst | 1 + .../document_processor_service.rst | 6 + docs/documentai_v1beta3/services.rst | 6 +- docs/documentai_v1beta3/types.rst | 1 + .../async_client.py | 21 +-- .../document_understanding_service/client.py | 40 +++-- .../documentai_v1beta2/types/document.py | 106 ++++++------ .../types/document_understanding.py | 38 ++--- .../documentai_v1beta2/types/geometry.py | 4 +- .../async_client.py | 20 ++- .../document_processor_service/client.py | 47 ++++-- .../documentai_v1beta3/types/document.py | 152 +++++++++--------- .../types/document_processor_service.py | 26 +-- .../documentai_v1beta3/types/geometry.py | 4 +- synth.metadata | 9 +- .../test_document_understanding_service.py | 28 +++- .../test_document_processor_service.py | 28 +++- 20 files changed, 319 insertions(+), 232 deletions(-) create mode 100644 docs/documentai_v1beta2/document_understanding_service.rst create mode 100644 docs/documentai_v1beta3/document_processor_service.rst diff --git a/.coveragerc b/.coveragerc index 8620651a..2c6e2b9a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -15,4 +15,4 @@ exclude_lines = # This is added at the module level as a safeguard for if someone # generates the code and tries to run it without pip installing. This # makes it virtually impossible to test properly. - except pkg_resources.DistributionNotFound \ No newline at end of file + except pkg_resources.DistributionNotFound diff --git a/docs/documentai_v1beta2/document_understanding_service.rst b/docs/documentai_v1beta2/document_understanding_service.rst new file mode 100644 index 00000000..a0d0da7e --- /dev/null +++ b/docs/documentai_v1beta2/document_understanding_service.rst @@ -0,0 +1,6 @@ +DocumentUnderstandingService +---------------------------------------------- + +.. automodule:: google.cloud.documentai_v1beta2.services.document_understanding_service + :members: + :inherited-members: diff --git a/docs/documentai_v1beta2/services.rst b/docs/documentai_v1beta2/services.rst index b1f00952..13f4a238 100644 --- a/docs/documentai_v1beta2/services.rst +++ b/docs/documentai_v1beta2/services.rst @@ -1,6 +1,6 @@ Services for Google Cloud Documentai v1beta2 API ================================================ +.. toctree:: + :maxdepth: 2 -.. automodule:: google.cloud.documentai_v1beta2.services.document_understanding_service - :members: - :inherited-members: + document_understanding_service diff --git a/docs/documentai_v1beta2/types.rst b/docs/documentai_v1beta2/types.rst index 35540dd0..9edede43 100644 --- a/docs/documentai_v1beta2/types.rst +++ b/docs/documentai_v1beta2/types.rst @@ -3,4 +3,5 @@ Types for Google Cloud Documentai v1beta2 API .. automodule:: google.cloud.documentai_v1beta2.types :members: + :undoc-members: :show-inheritance: diff --git a/docs/documentai_v1beta3/document_processor_service.rst b/docs/documentai_v1beta3/document_processor_service.rst new file mode 100644 index 00000000..6a2caa2b --- /dev/null +++ b/docs/documentai_v1beta3/document_processor_service.rst @@ -0,0 +1,6 @@ +DocumentProcessorService +------------------------------------------ + +.. automodule:: google.cloud.documentai_v1beta3.services.document_processor_service + :members: + :inherited-members: diff --git a/docs/documentai_v1beta3/services.rst b/docs/documentai_v1beta3/services.rst index b4a1011c..d19a944b 100644 --- a/docs/documentai_v1beta3/services.rst +++ b/docs/documentai_v1beta3/services.rst @@ -1,6 +1,6 @@ Services for Google Cloud Documentai v1beta3 API ================================================ +.. toctree:: + :maxdepth: 2 -.. automodule:: google.cloud.documentai_v1beta3.services.document_processor_service - :members: - :inherited-members: + document_processor_service diff --git a/docs/documentai_v1beta3/types.rst b/docs/documentai_v1beta3/types.rst index 31b489da..7e22aabc 100644 --- a/docs/documentai_v1beta3/types.rst +++ b/docs/documentai_v1beta3/types.rst @@ -3,4 +3,5 @@ Types for Google Cloud Documentai v1beta3 API .. automodule:: google.cloud.documentai_v1beta3.types :members: + :undoc-members: :show-inheritance: diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py index c961662b..e57c3180 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py @@ -85,6 +85,9 @@ class DocumentUnderstandingServiceAsyncClient: DocumentUnderstandingServiceClient.parse_common_location_path ) + from_service_account_info = ( + DocumentUnderstandingServiceClient.from_service_account_info + ) from_service_account_file = ( DocumentUnderstandingServiceClient.from_service_account_file ) @@ -165,13 +168,14 @@ async def batch_process_documents( written to Cloud Storage as JSON in the [Document] format. Args: - request (:class:`~.document_understanding.BatchProcessDocumentsRequest`): + request (:class:`google.cloud.documentai_v1beta2.types.BatchProcessDocumentsRequest`): The request object. Request to batch process documents as an asynchronous operation. The output is written to Cloud Storage as JSON in the [Document] format. - requests (:class:`Sequence[~.document_understanding.ProcessDocumentRequest]`): + requests (:class:`Sequence[google.cloud.documentai_v1beta2.types.ProcessDocumentRequest]`): Required. Individual requests for each document. + This corresponds to the ``requests`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -183,14 +187,11 @@ async def batch_process_documents( sent along with the request as metadata. Returns: - ~.operation_async.AsyncOperation: + google.api_core.operation_async.AsyncOperation: An object representing a long-running operation. - The result type for the operation will be - :class:``~.document_understanding.BatchProcessDocumentsResponse``: - Response to an batch document processing request. This - is returned in the LRO Operation after the operation is - complete. + The result type for the operation will be :class:`google.cloud.documentai_v1beta2.types.BatchProcessDocumentsResponse` Response to an batch document processing request. This is returned in + the LRO Operation after the operation is complete. """ # Create or coerce a protobuf request object. @@ -258,7 +259,7 @@ async def process_document( r"""Processes a single document. Args: - request (:class:`~.document_understanding.ProcessDocumentRequest`): + request (:class:`google.cloud.documentai_v1beta2.types.ProcessDocumentRequest`): The request object. Request to process one document. retry (google.api_core.retry.Retry): Designation of what errors, if any, @@ -268,7 +269,7 @@ async def process_document( sent along with the request as metadata. Returns: - ~.document.Document: + google.cloud.documentai_v1beta2.types.Document: Document represents the canonical document resource in Document Understanding AI. It is an interchange diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py index 0d740e5b..d417e2e0 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py @@ -122,6 +122,22 @@ def _get_default_mtls_endpoint(api_endpoint): DEFAULT_ENDPOINT ) + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + DocumentUnderstandingServiceClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_info(info) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + @classmethod def from_service_account_file(cls, filename: str, *args, **kwargs): """Creates an instance of this client using the provided credentials @@ -134,7 +150,7 @@ def from_service_account_file(cls, filename: str, *args, **kwargs): kwargs: Additional arguments to pass to the constructor. Returns: - {@api.name}: The constructed client. + DocumentUnderstandingServiceClient: The constructed client. """ credentials = service_account.Credentials.from_service_account_file(filename) kwargs["credentials"] = credentials @@ -226,10 +242,10 @@ def __init__( credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. - transport (Union[str, ~.DocumentUnderstandingServiceTransport]): The + transport (Union[str, DocumentUnderstandingServiceTransport]): The transport to use. If set to None, a transport is chosen automatically. - client_options (client_options_lib.ClientOptions): Custom options for the + client_options (google.api_core.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_ENDPOINT @@ -340,13 +356,14 @@ def batch_process_documents( written to Cloud Storage as JSON in the [Document] format. Args: - request (:class:`~.document_understanding.BatchProcessDocumentsRequest`): + request (google.cloud.documentai_v1beta2.types.BatchProcessDocumentsRequest): The request object. Request to batch process documents as an asynchronous operation. The output is written to Cloud Storage as JSON in the [Document] format. - requests (:class:`Sequence[~.document_understanding.ProcessDocumentRequest]`): + requests (Sequence[google.cloud.documentai_v1beta2.types.ProcessDocumentRequest]): Required. Individual requests for each document. + This corresponds to the ``requests`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -358,14 +375,11 @@ def batch_process_documents( sent along with the request as metadata. Returns: - ~.operation.Operation: + google.api_core.operation.Operation: An object representing a long-running operation. - The result type for the operation will be - :class:``~.document_understanding.BatchProcessDocumentsResponse``: - Response to an batch document processing request. This - is returned in the LRO Operation after the operation is - complete. + The result type for the operation will be :class:`google.cloud.documentai_v1beta2.types.BatchProcessDocumentsResponse` Response to an batch document processing request. This is returned in + the LRO Operation after the operation is complete. """ # Create or coerce a protobuf request object. @@ -426,7 +440,7 @@ def process_document( r"""Processes a single document. Args: - request (:class:`~.document_understanding.ProcessDocumentRequest`): + request (google.cloud.documentai_v1beta2.types.ProcessDocumentRequest): The request object. Request to process one document. retry (google.api_core.retry.Retry): Designation of what errors, if any, @@ -436,7 +450,7 @@ def process_document( sent along with the request as metadata. Returns: - ~.document.Document: + google.cloud.documentai_v1beta2.types.Document: Document represents the canonical document resource in Document Understanding AI. It is an interchange diff --git a/google/cloud/documentai_v1beta2/types/document.py b/google/cloud/documentai_v1beta2/types/document.py index 7b7d15af..e2411002 100644 --- a/google/cloud/documentai_v1beta2/types/document.py +++ b/google/cloud/documentai_v1beta2/types/document.py @@ -55,29 +55,29 @@ class Document(proto.Message): text (str): UTF-8 encoded text in reading order from the document. - text_styles (Sequence[~.document.Document.Style]): + text_styles (Sequence[google.cloud.documentai_v1beta2.types.Document.Style]): Styles for the [Document.text][google.cloud.documentai.v1beta2.Document.text]. - pages (Sequence[~.document.Document.Page]): + pages (Sequence[google.cloud.documentai_v1beta2.types.Document.Page]): Visual page layout for the [Document][google.cloud.documentai.v1beta2.Document]. - entities (Sequence[~.document.Document.Entity]): + entities (Sequence[google.cloud.documentai_v1beta2.types.Document.Entity]): A list of entities detected on [Document.text][google.cloud.documentai.v1beta2.Document.text]. For document shards, entities in this list may cross shard boundaries. - entity_relations (Sequence[~.document.Document.EntityRelation]): + entity_relations (Sequence[google.cloud.documentai_v1beta2.types.Document.EntityRelation]): Relationship among [Document.entities][google.cloud.documentai.v1beta2.Document.entities]. - shard_info (~.document.Document.ShardInfo): + shard_info (google.cloud.documentai_v1beta2.types.Document.ShardInfo): Information about the sharding if this document is sharded part of a larger document. If the document is not sharded, this message is not specified. - labels (Sequence[~.document.Document.Label]): + labels (Sequence[google.cloud.documentai_v1beta2.types.Document.Label]): [Label][google.cloud.documentai.v1beta2.Document.Label]s for this document. - error (~.status.Status): + error (google.rpc.status_pb2.Status): Any error that occurred while processing this document. """ @@ -140,12 +140,12 @@ class Style(proto.Message): CSS conventions as much as possible. Attributes: - text_anchor (~.document.Document.TextAnchor): + text_anchor (google.cloud.documentai_v1beta2.types.Document.TextAnchor): Text anchor indexing into the [Document.text][google.cloud.documentai.v1beta2.Document.text]. - color (~.gt_color.Color): + color (google.type.color_pb2.Color): Text color. - background_color (~.gt_color.Color): + background_color (google.type.color_pb2.Color): Text background color. font_weight (str): Font weight. Possible values are normal, bold, bolder, and @@ -156,7 +156,7 @@ class Style(proto.Message): text_decoration (str): Text decoration. Follows CSS standard. https://www.w3schools.com/cssref/pr_text_text-decoration.asp - font_size (~.document.Document.Style.FontSize): + font_size (google.cloud.documentai_v1beta2.types.Document.Style.FontSize): Font size. """ @@ -204,37 +204,37 @@ class Page(proto.Message): Useful when a page is taken out of a [Document][google.cloud.documentai.v1beta2.Document] for individual processing. - dimension (~.document.Document.Page.Dimension): + dimension (google.cloud.documentai_v1beta2.types.Document.Page.Dimension): Physical dimension of the page. - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta2.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta2.Document.Page.Layout] for the page. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. - blocks (Sequence[~.document.Document.Page.Block]): + blocks (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.Block]): A list of visually detected text blocks on the page. A block has a set of lines (collected into paragraphs) that have a common line-spacing and orientation. - paragraphs (Sequence[~.document.Document.Page.Paragraph]): + paragraphs (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.Paragraph]): A list of visually detected text paragraphs on the page. A collection of lines that a human would perceive as a paragraph. - lines (Sequence[~.document.Document.Page.Line]): + lines (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.Line]): A list of visually detected text lines on the page. A collection of tokens that a human would perceive as a line. - tokens (Sequence[~.document.Document.Page.Token]): + tokens (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.Token]): A list of visually detected tokens on the page. - visual_elements (Sequence[~.document.Document.Page.VisualElement]): + visual_elements (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.VisualElement]): A list of detected non-text visual elements e.g. checkbox, signature etc. on the page. - tables (Sequence[~.document.Document.Page.Table]): + tables (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.Table]): A list of visually detected tables on the page. - form_fields (Sequence[~.document.Document.Page.FormField]): + form_fields (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.FormField]): A list of visually detected form fields on the page. """ @@ -261,7 +261,7 @@ class Layout(proto.Message): r"""Visual element describing a layout unit on a page. Attributes: - text_anchor (~.document.Document.TextAnchor): + text_anchor (google.cloud.documentai_v1beta2.types.Document.TextAnchor): Text anchor indexing into the [Document.text][google.cloud.documentai.v1beta2.Document.text]. confidence (float): @@ -270,10 +270,10 @@ class Layout(proto.Message): within context of the object this layout is for. e.g. confidence can be for a single token, a table, a visual element, etc. depending on context. Range [0, 1]. - bounding_poly (~.geometry.BoundingPoly): + bounding_poly (google.cloud.documentai_v1beta2.types.BoundingPoly): The bounding polygon for the [Layout][google.cloud.documentai.v1beta2.Document.Page.Layout]. - orientation (~.document.Document.Page.Layout.Orientation): + orientation (google.cloud.documentai_v1beta2.types.Document.Page.Layout.Orientation): Detected orientation for the [Layout][google.cloud.documentai.v1beta2.Document.Page.Layout]. id (str): @@ -310,11 +310,11 @@ class Block(proto.Message): have a common line-spacing and orientation. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta2.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta2.Document.Page.Layout] for [Block][google.cloud.documentai.v1beta2.Document.Page.Block]. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. """ @@ -332,11 +332,11 @@ class Paragraph(proto.Message): paragraph. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta2.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta2.Document.Page.Layout] for [Paragraph][google.cloud.documentai.v1beta2.Document.Page.Paragraph]. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. """ @@ -355,11 +355,11 @@ class Line(proto.Message): etc. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta2.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta2.Document.Page.Layout] for [Line][google.cloud.documentai.v1beta2.Document.Page.Line]. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. """ @@ -376,14 +376,14 @@ class Token(proto.Message): r"""A detected token. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta2.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta2.Document.Page.Layout] for [Token][google.cloud.documentai.v1beta2.Document.Page.Token]. - detected_break (~.document.Document.Page.Token.DetectedBreak): + detected_break (google.cloud.documentai_v1beta2.types.Document.Page.Token.DetectedBreak): Detected break at the end of a [Token][google.cloud.documentai.v1beta2.Document.Page.Token]. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. """ @@ -393,7 +393,7 @@ class DetectedBreak(proto.Message): [Token][google.cloud.documentai.v1beta2.Document.Page.Token]. Attributes: - type_ (~.document.Document.Page.Token.DetectedBreak.Type): + type_ (google.cloud.documentai_v1beta2.types.Document.Page.Token.DetectedBreak.Type): Detected break type. """ @@ -425,14 +425,14 @@ class VisualElement(proto.Message): etc. on the page. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta2.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta2.Document.Page.Layout] for [VisualElement][google.cloud.documentai.v1beta2.Document.Page.VisualElement]. type_ (str): Type of the [VisualElement][google.cloud.documentai.v1beta2.Document.Page.VisualElement]. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. """ @@ -451,15 +451,15 @@ class Table(proto.Message): r"""A table representation similar to HTML table structure. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta2.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta2.Document.Page.Layout] for [Table][google.cloud.documentai.v1beta2.Document.Page.Table]. - header_rows (Sequence[~.document.Document.Page.Table.TableRow]): + header_rows (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.Table.TableRow]): Header rows of the table. - body_rows (Sequence[~.document.Document.Page.Table.TableRow]): + body_rows (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.Table.TableRow]): Body rows of the table. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. """ @@ -468,7 +468,7 @@ class TableRow(proto.Message): r"""A row of table cells. Attributes: - cells (Sequence[~.document.Document.Page.Table.TableCell]): + cells (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.Table.TableCell]): Cells that make up this row. """ @@ -480,7 +480,7 @@ class TableCell(proto.Message): r"""A cell representation inside the table. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta2.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta2.Document.Page.Layout] for [TableCell][google.cloud.documentai.v1beta2.Document.Page.Table.TableCell]. @@ -488,7 +488,7 @@ class TableCell(proto.Message): How many rows this cell spans. col_span (int): How many columns this cell spans. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. """ @@ -525,21 +525,21 @@ class FormField(proto.Message): r"""A form field detected on the page. Attributes: - field_name (~.document.Document.Page.Layout): + field_name (google.cloud.documentai_v1beta2.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta2.Document.Page.Layout] for the [FormField][google.cloud.documentai.v1beta2.Document.Page.FormField] name. e.g. ``Address``, ``Email``, ``Grand total``, ``Phone number``, etc. - field_value (~.document.Document.Page.Layout): + field_value (google.cloud.documentai_v1beta2.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta2.Document.Page.Layout] for the [FormField][google.cloud.documentai.v1beta2.Document.Page.FormField] value. - name_detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + name_detected_languages (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.DetectedLanguage]): A list of detected languages for name together with confidence. - value_detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + value_detected_languages (Sequence[google.cloud.documentai_v1beta2.types.Document.Page.DetectedLanguage]): A list of detected languages for value together with confidence. value_type (str): @@ -640,7 +640,7 @@ class Entity(proto.Message): person, an organization, or location. Attributes: - text_anchor (~.document.Document.TextAnchor): + text_anchor (google.cloud.documentai_v1beta2.types.Document.TextAnchor): Provenance of the entity. Text anchor indexing into the [Document.text][google.cloud.documentai.v1beta2.Document.text]. type_ (str): @@ -652,14 +652,14 @@ class Entity(proto.Message): confidence (float): Optional. Confidence of detected Schema entity. Range [0, 1]. - page_anchor (~.document.Document.PageAnchor): + page_anchor (google.cloud.documentai_v1beta2.types.Document.PageAnchor): Optional. Represents the provenance of this entity wrt. the location on the page where it was found. id (str): Optional. Canonical id. This will be a unique value in the entity list for this document. - bounding_poly_for_demo_frontend (~.geometry.BoundingPoly): + bounding_poly_for_demo_frontend (google.cloud.documentai_v1beta2.types.BoundingPoly): Optional. Temporary field to store the bounding poly for short-term POCs. Used by the frontend only. Do not use before you talk to @@ -712,7 +712,7 @@ class TextAnchor(proto.Message): [Document.text][google.cloud.documentai.v1beta2.Document.text]. Attributes: - text_segments (Sequence[~.document.Document.TextAnchor.TextSegment]): + text_segments (Sequence[google.cloud.documentai_v1beta2.types.Document.TextAnchor.TextSegment]): The text segments from the [Document.text][google.cloud.documentai.v1beta2.Document.text]. """ @@ -748,7 +748,7 @@ class PageAnchor(proto.Message): [Document.pages][google.cloud.documentai.v1beta2.Document.pages]. Attributes: - page_refs (Sequence[~.document.Document.PageAnchor.PageRef]): + page_refs (Sequence[google.cloud.documentai_v1beta2.types.Document.PageAnchor.PageRef]): One or more references to visual page elements """ @@ -762,7 +762,7 @@ class PageRef(proto.Message): Required. Index into the [Document.pages][google.cloud.documentai.v1beta2.Document.pages] element - layout_type (~.document.Document.PageAnchor.PageRef.LayoutType): + layout_type (google.cloud.documentai_v1beta2.types.Document.PageAnchor.PageRef.LayoutType): Optional. The type of the layout element that is being referenced. If not specified the whole page is assumed to be referenced. diff --git a/google/cloud/documentai_v1beta2/types/document_understanding.py b/google/cloud/documentai_v1beta2/types/document_understanding.py index bdf2299f..dd80f269 100644 --- a/google/cloud/documentai_v1beta2/types/document_understanding.py +++ b/google/cloud/documentai_v1beta2/types/document_understanding.py @@ -50,7 +50,7 @@ class BatchProcessDocumentsRequest(proto.Message): output is written to Cloud Storage as JSON in the [Document] format. Attributes: - requests (Sequence[~.document_understanding.ProcessDocumentRequest]): + requests (Sequence[google.cloud.documentai_v1beta2.types.ProcessDocumentRequest]): Required. Individual requests for each document. parent (str): @@ -81,9 +81,9 @@ class ProcessDocumentRequest(proto.Message): If no location is specified, a region will be chosen automatically. This field is only populated when used in ProcessDocument method. - input_config (~.document_understanding.InputConfig): + input_config (google.cloud.documentai_v1beta2.types.InputConfig): Required. Information about the input file. - output_config (~.document_understanding.OutputConfig): + output_config (google.cloud.documentai_v1beta2.types.OutputConfig): Optional. The desired output location. This field is only needed in BatchProcessDocumentsRequest. @@ -93,22 +93,22 @@ class ProcessDocumentRequest(proto.Message): "general" and "invoice". If not provided, "general"\ is used as default. If any other value is given, the request is rejected. - table_extraction_params (~.document_understanding.TableExtractionParams): + table_extraction_params (google.cloud.documentai_v1beta2.types.TableExtractionParams): Controls table extraction behavior. If not specified, the system will decide reasonable defaults. - form_extraction_params (~.document_understanding.FormExtractionParams): + form_extraction_params (google.cloud.documentai_v1beta2.types.FormExtractionParams): Controls form extraction behavior. If not specified, the system will decide reasonable defaults. - entity_extraction_params (~.document_understanding.EntityExtractionParams): + entity_extraction_params (google.cloud.documentai_v1beta2.types.EntityExtractionParams): Controls entity extraction behavior. If not specified, the system will decide reasonable defaults. - ocr_params (~.document_understanding.OcrParams): + ocr_params (google.cloud.documentai_v1beta2.types.OcrParams): Controls OCR behavior. If not specified, the system will decide reasonable defaults. - automl_params (~.document_understanding.AutoMlParams): + automl_params (google.cloud.documentai_v1beta2.types.AutoMlParams): Controls AutoML model prediction behavior. AutoMlParams cannot be used together with other Params. @@ -144,7 +144,7 @@ class BatchProcessDocumentsResponse(proto.Message): returned in the LRO Operation after the operation is complete. Attributes: - responses (Sequence[~.document_understanding.ProcessDocumentResponse]): + responses (Sequence[google.cloud.documentai_v1beta2.types.ProcessDocumentResponse]): Responses for each individual document. """ @@ -157,11 +157,11 @@ class ProcessDocumentResponse(proto.Message): r"""Response to a single document processing request. Attributes: - input_config (~.document_understanding.InputConfig): + input_config (google.cloud.documentai_v1beta2.types.InputConfig): Information about the input file. This is the same as the corresponding input config in the request. - output_config (~.document_understanding.OutputConfig): + output_config (google.cloud.documentai_v1beta2.types.OutputConfig): The output location of the parsed responses. The responses are written to this location as JSON-serialized ``Document`` objects. @@ -198,7 +198,7 @@ class TableExtractionParams(proto.Message): Attributes: enabled (bool): Whether to enable table extraction. - table_bound_hints (Sequence[~.document_understanding.TableBoundHint]): + table_bound_hints (Sequence[google.cloud.documentai_v1beta2.types.TableBoundHint]): Optional. Table bounding box hints that can be provided to complex cases which our algorithm cannot locate the table(s) in. @@ -233,7 +233,7 @@ class TableBoundHint(proto.Message): this hint applies to. If not provided, this hint will apply to all pages by default. This value is 1-based. - bounding_box (~.geometry.BoundingPoly): + bounding_box (google.cloud.documentai_v1beta2.types.BoundingPoly): Bounding box hint for a table on this page. The coordinates must be normalized to [0,1] and the bounding box must be an axis-aligned rectangle. @@ -250,7 +250,7 @@ class FormExtractionParams(proto.Message): Attributes: enabled (bool): Whether to enable form extraction. - key_value_pair_hints (Sequence[~.document_understanding.KeyValuePairHint]): + key_value_pair_hints (Sequence[google.cloud.documentai_v1beta2.types.KeyValuePairHint]): User can provide pairs of (key text, value type) to improve the parsing result. @@ -336,7 +336,7 @@ class InputConfig(proto.Message): r"""The desired input location and metadata. Attributes: - gcs_source (~.document_understanding.GcsSource): + gcs_source (google.cloud.documentai_v1beta2.types.GcsSource): The Google Cloud Storage location to read the input from. This must be a single file. contents (bytes): @@ -369,7 +369,7 @@ class OutputConfig(proto.Message): r"""The desired output location and metadata. Attributes: - gcs_destination (~.document_understanding.GcsDestination): + gcs_destination (google.cloud.documentai_v1beta2.types.GcsDestination): The Google Cloud Storage location to write the output to. pages_per_shard (int): @@ -427,14 +427,14 @@ class OperationMetadata(proto.Message): r"""Contains metadata for the BatchProcessDocuments operation. Attributes: - state (~.document_understanding.OperationMetadata.State): + state (google.cloud.documentai_v1beta2.types.OperationMetadata.State): The state of the current batch processing. state_message (str): A message providing more details about the current state of processing. - create_time (~.timestamp.Timestamp): + create_time (google.protobuf.timestamp_pb2.Timestamp): The creation time of the operation. - update_time (~.timestamp.Timestamp): + update_time (google.protobuf.timestamp_pb2.Timestamp): The last update time of the operation. """ diff --git a/google/cloud/documentai_v1beta2/types/geometry.py b/google/cloud/documentai_v1beta2/types/geometry.py index 0592f336..38ae138a 100644 --- a/google/cloud/documentai_v1beta2/types/geometry.py +++ b/google/cloud/documentai_v1beta2/types/geometry.py @@ -62,9 +62,9 @@ class BoundingPoly(proto.Message): r"""A bounding polygon for the detected image annotation. Attributes: - vertices (Sequence[~.geometry.Vertex]): + vertices (Sequence[google.cloud.documentai_v1beta2.types.Vertex]): The bounding polygon vertices. - normalized_vertices (Sequence[~.geometry.NormalizedVertex]): + normalized_vertices (Sequence[google.cloud.documentai_v1beta2.types.NormalizedVertex]): The bounding polygon normalized vertices. """ diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py b/google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py index 467b072a..a2077e22 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py @@ -95,6 +95,7 @@ class DocumentProcessorServiceAsyncClient: DocumentProcessorServiceClient.parse_common_location_path ) + from_service_account_info = DocumentProcessorServiceClient.from_service_account_info from_service_account_file = DocumentProcessorServiceClient.from_service_account_file from_service_account_json = from_service_account_file @@ -172,12 +173,13 @@ async def process_document( r"""Processes a single document. Args: - request (:class:`~.document_processor_service.ProcessRequest`): + request (:class:`google.cloud.documentai_v1beta3.types.ProcessRequest`): The request object. Request message for the process document method. name (:class:`str`): Required. The processor resource name. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -189,7 +191,7 @@ async def process_document( sent along with the request as metadata. Returns: - ~.document_processor_service.ProcessResponse: + google.cloud.documentai_v1beta3.types.ProcessResponse: Response message for the process document method. @@ -253,12 +255,13 @@ async def batch_process_documents( written to Cloud Storage as JSON in the [Document] format. Args: - request (:class:`~.document_processor_service.BatchProcessRequest`): + request (:class:`google.cloud.documentai_v1beta3.types.BatchProcessRequest`): The request object. Request message for batch process document method. name (:class:`str`): Required. The processor resource name. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -270,11 +273,11 @@ async def batch_process_documents( sent along with the request as metadata. Returns: - ~.operation_async.AsyncOperation: + google.api_core.operation_async.AsyncOperation: An object representing a long-running operation. The result type for the operation will be - :class:``~.document_processor_service.BatchProcessResponse``: + :class:`google.cloud.documentai_v1beta3.types.BatchProcessResponse` Response message for batch process document method. """ @@ -345,13 +348,14 @@ async def review_document( should be processed by the specified processor. Args: - request (:class:`~.document_processor_service.ReviewDocumentRequest`): + request (:class:`google.cloud.documentai_v1beta3.types.ReviewDocumentRequest`): The request object. Request message for review document method. human_review_config (:class:`str`): Required. The resource name of the HumanReviewConfig that the document will be reviewed with. + This corresponds to the ``human_review_config`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -363,11 +367,11 @@ async def review_document( sent along with the request as metadata. Returns: - ~.operation_async.AsyncOperation: + google.api_core.operation_async.AsyncOperation: An object representing a long-running operation. The result type for the operation will be - :class:``~.document_processor_service.ReviewDocumentResponse``: + :class:`google.cloud.documentai_v1beta3.types.ReviewDocumentResponse` Response message for review document method. """ diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/client.py b/google/cloud/documentai_v1beta3/services/document_processor_service/client.py index e621e251..3c039826 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/client.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/client.py @@ -119,6 +119,22 @@ def _get_default_mtls_endpoint(api_endpoint): DEFAULT_ENDPOINT ) + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + DocumentProcessorServiceClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_info(info) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + @classmethod def from_service_account_file(cls, filename: str, *args, **kwargs): """Creates an instance of this client using the provided credentials @@ -131,7 +147,7 @@ def from_service_account_file(cls, filename: str, *args, **kwargs): kwargs: Additional arguments to pass to the constructor. Returns: - {@api.name}: The constructed client. + DocumentProcessorServiceClient: The constructed client. """ credentials = service_account.Credentials.from_service_account_file(filename) kwargs["credentials"] = credentials @@ -255,10 +271,10 @@ def __init__( credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. - transport (Union[str, ~.DocumentProcessorServiceTransport]): The + transport (Union[str, DocumentProcessorServiceTransport]): The transport to use. If set to None, a transport is chosen automatically. - client_options (client_options_lib.ClientOptions): Custom options for the + client_options (google.api_core.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_ENDPOINT @@ -368,12 +384,13 @@ def process_document( r"""Processes a single document. Args: - request (:class:`~.document_processor_service.ProcessRequest`): + request (google.cloud.documentai_v1beta3.types.ProcessRequest): The request object. Request message for the process document method. - name (:class:`str`): + name (str): Required. The processor resource name. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -385,7 +402,7 @@ def process_document( sent along with the request as metadata. Returns: - ~.document_processor_service.ProcessResponse: + google.cloud.documentai_v1beta3.types.ProcessResponse: Response message for the process document method. @@ -442,12 +459,13 @@ def batch_process_documents( written to Cloud Storage as JSON in the [Document] format. Args: - request (:class:`~.document_processor_service.BatchProcessRequest`): + request (google.cloud.documentai_v1beta3.types.BatchProcessRequest): The request object. Request message for batch process document method. - name (:class:`str`): + name (str): Required. The processor resource name. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -459,11 +477,11 @@ def batch_process_documents( sent along with the request as metadata. Returns: - ~.operation.Operation: + google.api_core.operation.Operation: An object representing a long-running operation. The result type for the operation will be - :class:``~.document_processor_service.BatchProcessResponse``: + :class:`google.cloud.documentai_v1beta3.types.BatchProcessResponse` Response message for batch process document method. """ @@ -527,13 +545,14 @@ def review_document( should be processed by the specified processor. Args: - request (:class:`~.document_processor_service.ReviewDocumentRequest`): + request (google.cloud.documentai_v1beta3.types.ReviewDocumentRequest): The request object. Request message for review document method. - human_review_config (:class:`str`): + human_review_config (str): Required. The resource name of the HumanReviewConfig that the document will be reviewed with. + This corresponds to the ``human_review_config`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -545,11 +564,11 @@ def review_document( sent along with the request as metadata. Returns: - ~.operation.Operation: + google.api_core.operation.Operation: An object representing a long-running operation. The result type for the operation will be - :class:``~.document_processor_service.ReviewDocumentResponse``: + :class:`google.cloud.documentai_v1beta3.types.ReviewDocumentResponse` Response message for review document method. """ diff --git a/google/cloud/documentai_v1beta3/types/document.py b/google/cloud/documentai_v1beta3/types/document.py index 104a366c..f979c519 100644 --- a/google/cloud/documentai_v1beta3/types/document.py +++ b/google/cloud/documentai_v1beta3/types/document.py @@ -60,39 +60,39 @@ class Document(proto.Message): text (str): UTF-8 encoded text in reading order from the document. - text_styles (Sequence[~.document.Document.Style]): + text_styles (Sequence[google.cloud.documentai_v1beta3.types.Document.Style]): Styles for the [Document.text][google.cloud.documentai.v1beta3.Document.text]. - pages (Sequence[~.document.Document.Page]): + pages (Sequence[google.cloud.documentai_v1beta3.types.Document.Page]): Visual page layout for the [Document][google.cloud.documentai.v1beta3.Document]. - entities (Sequence[~.document.Document.Entity]): + entities (Sequence[google.cloud.documentai_v1beta3.types.Document.Entity]): A list of entities detected on [Document.text][google.cloud.documentai.v1beta3.Document.text]. For document shards, entities in this list may cross shard boundaries. - entity_relations (Sequence[~.document.Document.EntityRelation]): + entity_relations (Sequence[google.cloud.documentai_v1beta3.types.Document.EntityRelation]): Relationship among [Document.entities][google.cloud.documentai.v1beta3.Document.entities]. - translations (Sequence[~.document.Document.Translation]): + translations (Sequence[google.cloud.documentai_v1beta3.types.Document.Translation]): A list of translations on [Document.text][google.cloud.documentai.v1beta3.Document.text]. For document shards, translations in this list may cross shard boundaries. - text_changes (Sequence[~.document.Document.TextChange]): + text_changes (Sequence[google.cloud.documentai_v1beta3.types.Document.TextChange]): A list of text corrections made to [Document.text]. This is usually used for annotating corrections to OCR mistakes. Text changes for a given revision may not overlap with each other. - shard_info (~.document.Document.ShardInfo): + shard_info (google.cloud.documentai_v1beta3.types.Document.ShardInfo): Information about the sharding if this document is sharded part of a larger document. If the document is not sharded, this message is not specified. - error (~.status.Status): + error (google.rpc.status_pb2.Status): Any error that occurred while processing this document. - revisions (Sequence[~.document.Document.Revision]): + revisions (Sequence[google.cloud.documentai_v1beta3.types.Document.Revision]): Revision history of this document. """ @@ -123,12 +123,12 @@ class Style(proto.Message): CSS conventions as much as possible. Attributes: - text_anchor (~.document.Document.TextAnchor): + text_anchor (google.cloud.documentai_v1beta3.types.Document.TextAnchor): Text anchor indexing into the [Document.text][google.cloud.documentai.v1beta3.Document.text]. - color (~.gt_color.Color): + color (google.type.color_pb2.Color): Text color. - background_color (~.gt_color.Color): + background_color (google.type.color_pb2.Color): Text background color. font_weight (str): Font weight. Possible values are normal, bold, bolder, and @@ -139,7 +139,7 @@ class Style(proto.Message): text_decoration (str): Text decoration. Follows CSS standard. https://www.w3schools.com/cssref/pr_text_text-decoration.asp - font_size (~.document.Document.Style.FontSize): + font_size (google.cloud.documentai_v1beta3.types.Document.Style.FontSize): Font size. """ @@ -187,46 +187,46 @@ class Page(proto.Message): Useful when a page is taken out of a [Document][google.cloud.documentai.v1beta3.Document] for individual processing. - image (~.document.Document.Page.Image): + image (google.cloud.documentai_v1beta3.types.Document.Page.Image): Rendered image for this page. This image is preprocessed to remove any skew, rotation, and distortions such that the annotation bounding boxes can be upright and axis-aligned. - transforms (Sequence[~.document.Document.Page.Matrix]): + transforms (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.Matrix]): Transformation matrices that were applied to the original document image to produce [Page.image][google.cloud.documentai.v1beta3.Document.Page.image]. - dimension (~.document.Document.Page.Dimension): + dimension (google.cloud.documentai_v1beta3.types.Document.Page.Dimension): Physical dimension of the page. - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta3.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta3.Document.Page.Layout] for the page. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. - blocks (Sequence[~.document.Document.Page.Block]): + blocks (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.Block]): A list of visually detected text blocks on the page. A block has a set of lines (collected into paragraphs) that have a common line-spacing and orientation. - paragraphs (Sequence[~.document.Document.Page.Paragraph]): + paragraphs (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.Paragraph]): A list of visually detected text paragraphs on the page. A collection of lines that a human would perceive as a paragraph. - lines (Sequence[~.document.Document.Page.Line]): + lines (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.Line]): A list of visually detected text lines on the page. A collection of tokens that a human would perceive as a line. - tokens (Sequence[~.document.Document.Page.Token]): + tokens (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.Token]): A list of visually detected tokens on the page. - visual_elements (Sequence[~.document.Document.Page.VisualElement]): + visual_elements (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.VisualElement]): A list of detected non-text visual elements e.g. checkbox, signature etc. on the page. - tables (Sequence[~.document.Document.Page.Table]): + tables (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.Table]): A list of visually detected tables on the page. - form_fields (Sequence[~.document.Document.Page.FormField]): + form_fields (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.FormField]): A list of visually detected form fields on the page. """ @@ -302,7 +302,7 @@ class Layout(proto.Message): r"""Visual element describing a layout unit on a page. Attributes: - text_anchor (~.document.Document.TextAnchor): + text_anchor (google.cloud.documentai_v1beta3.types.Document.TextAnchor): Text anchor indexing into the [Document.text][google.cloud.documentai.v1beta3.Document.text]. confidence (float): @@ -311,10 +311,10 @@ class Layout(proto.Message): within context of the object this layout is for. e.g. confidence can be for a single token, a table, a visual element, etc. depending on context. Range [0, 1]. - bounding_poly (~.geometry.BoundingPoly): + bounding_poly (google.cloud.documentai_v1beta3.types.BoundingPoly): The bounding polygon for the [Layout][google.cloud.documentai.v1beta3.Document.Page.Layout]. - orientation (~.document.Document.Page.Layout.Orientation): + orientation (google.cloud.documentai_v1beta3.types.Document.Page.Layout.Orientation): Detected orientation for the [Layout][google.cloud.documentai.v1beta3.Document.Page.Layout]. """ @@ -346,14 +346,14 @@ class Block(proto.Message): have a common line-spacing and orientation. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta3.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta3.Document.Page.Layout] for [Block][google.cloud.documentai.v1beta3.Document.Page.Block]. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. - provenance (~.document.Document.Provenance): + provenance (google.cloud.documentai_v1beta3.types.Document.Provenance): The history of this annotation. """ @@ -374,14 +374,14 @@ class Paragraph(proto.Message): paragraph. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta3.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta3.Document.Page.Layout] for [Paragraph][google.cloud.documentai.v1beta3.Document.Page.Paragraph]. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. - provenance (~.document.Document.Provenance): + provenance (google.cloud.documentai_v1beta3.types.Document.Provenance): The history of this annotation. """ @@ -403,14 +403,14 @@ class Line(proto.Message): etc. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta3.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta3.Document.Page.Layout] for [Line][google.cloud.documentai.v1beta3.Document.Page.Line]. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. - provenance (~.document.Document.Provenance): + provenance (google.cloud.documentai_v1beta3.types.Document.Provenance): The history of this annotation. """ @@ -430,17 +430,17 @@ class Token(proto.Message): r"""A detected token. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta3.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta3.Document.Page.Layout] for [Token][google.cloud.documentai.v1beta3.Document.Page.Token]. - detected_break (~.document.Document.Page.Token.DetectedBreak): + detected_break (google.cloud.documentai_v1beta3.types.Document.Page.Token.DetectedBreak): Detected break at the end of a [Token][google.cloud.documentai.v1beta3.Document.Page.Token]. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. - provenance (~.document.Document.Provenance): + provenance (google.cloud.documentai_v1beta3.types.Document.Provenance): The history of this annotation. """ @@ -449,7 +449,7 @@ class DetectedBreak(proto.Message): [Token][google.cloud.documentai.v1beta3.Document.Page.Token]. Attributes: - type_ (~.document.Document.Page.Token.DetectedBreak.Type): + type_ (google.cloud.documentai_v1beta3.types.Document.Page.Token.DetectedBreak.Type): Detected break type. """ @@ -485,14 +485,14 @@ class VisualElement(proto.Message): etc. on the page. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta3.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta3.Document.Page.Layout] for [VisualElement][google.cloud.documentai.v1beta3.Document.Page.VisualElement]. type_ (str): Type of the [VisualElement][google.cloud.documentai.v1beta3.Document.Page.VisualElement]. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. """ @@ -511,15 +511,15 @@ class Table(proto.Message): r"""A table representation similar to HTML table structure. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta3.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta3.Document.Page.Layout] for [Table][google.cloud.documentai.v1beta3.Document.Page.Table]. - header_rows (Sequence[~.document.Document.Page.Table.TableRow]): + header_rows (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.Table.TableRow]): Header rows of the table. - body_rows (Sequence[~.document.Document.Page.Table.TableRow]): + body_rows (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.Table.TableRow]): Body rows of the table. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. """ @@ -528,7 +528,7 @@ class TableRow(proto.Message): r"""A row of table cells. Attributes: - cells (Sequence[~.document.Document.Page.Table.TableCell]): + cells (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.Table.TableCell]): Cells that make up this row. """ @@ -540,7 +540,7 @@ class TableCell(proto.Message): r"""A cell representation inside the table. Attributes: - layout (~.document.Document.Page.Layout): + layout (google.cloud.documentai_v1beta3.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta3.Document.Page.Layout] for [TableCell][google.cloud.documentai.v1beta3.Document.Page.Table.TableCell]. @@ -548,7 +548,7 @@ class TableCell(proto.Message): How many rows this cell spans. col_span (int): How many columns this cell spans. - detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + detected_languages (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.DetectedLanguage]): A list of detected languages together with confidence. """ @@ -585,21 +585,21 @@ class FormField(proto.Message): r"""A form field detected on the page. Attributes: - field_name (~.document.Document.Page.Layout): + field_name (google.cloud.documentai_v1beta3.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta3.Document.Page.Layout] for the [FormField][google.cloud.documentai.v1beta3.Document.Page.FormField] name. e.g. ``Address``, ``Email``, ``Grand total``, ``Phone number``, etc. - field_value (~.document.Document.Page.Layout): + field_value (google.cloud.documentai_v1beta3.types.Document.Page.Layout): [Layout][google.cloud.documentai.v1beta3.Document.Page.Layout] for the [FormField][google.cloud.documentai.v1beta3.Document.Page.FormField] value. - name_detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + name_detected_languages (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.DetectedLanguage]): A list of detected languages for name together with confidence. - value_detected_languages (Sequence[~.document.Document.Page.DetectedLanguage]): + value_detected_languages (Sequence[google.cloud.documentai_v1beta3.types.Document.Page.DetectedLanguage]): A list of detected languages for value together with confidence. value_type (str): @@ -696,7 +696,7 @@ class Entity(proto.Message): person, an organization, or location. Attributes: - text_anchor (~.document.Document.TextAnchor): + text_anchor (google.cloud.documentai_v1beta3.types.Document.TextAnchor): Provenance of the entity. Text anchor indexing into the [Document.text][google.cloud.documentai.v1beta3.Document.text]. type_ (str): @@ -708,24 +708,24 @@ class Entity(proto.Message): confidence (float): Optional. Confidence of detected Schema entity. Range [0, 1]. - page_anchor (~.document.Document.PageAnchor): + page_anchor (google.cloud.documentai_v1beta3.types.Document.PageAnchor): Optional. Represents the provenance of this entity wrt. the location on the page where it was found. id (str): Canonical id. This will be a unique value in the entity list for this document. - normalized_value (~.document.Document.Entity.NormalizedValue): + normalized_value (google.cloud.documentai_v1beta3.types.Document.Entity.NormalizedValue): Optional. Normalized entity value. Absent if the extracted value could not be converted or the type (e.g. address) is not supported for certain parsers. This field is also only populated for certain supported document types. - properties (Sequence[~.document.Document.Entity]): + properties (Sequence[google.cloud.documentai_v1beta3.types.Document.Entity]): Optional. Entities can be nested to form a hierarchical data structure representing the content in the document. - provenance (~.document.Document.Provenance): + provenance (google.cloud.documentai_v1beta3.types.Document.Provenance): Optional. The history of this annotation. redacted (bool): Optional. Whether the entity will be redacted @@ -736,21 +736,21 @@ class NormalizedValue(proto.Message): r"""Parsed and normalized entity value. Attributes: - money_value (~.money.Money): + money_value (google.type.money_pb2.Money): Money value. See also: https: github.com/googleapis/googleapis/blob/master/google/type/money.proto - date_value (~.date.Date): + date_value (google.type.date_pb2.Date): Date value. Includes year, month, day. See also: https: github.com/googleapis/googleapis/blob/master/google/type/date.proto - datetime_value (~.datetime.DateTime): + datetime_value (google.type.datetime_pb2.DateTime): DateTime value. Includes date, time, and timezone. See also: https: github.com/googleapis/googleapis/blob/master/google/type/datetime.proto - address_value (~.postal_address.PostalAddress): + address_value (google.type.postal_address_pb2.PostalAddress): Postal address. See also: https: @@ -848,7 +848,7 @@ class Translation(proto.Message): r"""A translation of the text segment. Attributes: - text_anchor (~.document.Document.TextAnchor): + text_anchor (google.cloud.documentai_v1beta3.types.Document.TextAnchor): Provenance of the translation. Text anchor indexing into the [Document.text][google.cloud.documentai.v1beta3.Document.text]. There can only be a single ``TextAnchor.text_segments`` @@ -860,7 +860,7 @@ class Translation(proto.Message): http://www.unicode.org/reports/tr35/#Unicode_locale_identifier. translated_text (str): Text translated into the target language. - provenance (Sequence[~.document.Document.Provenance]): + provenance (Sequence[google.cloud.documentai_v1beta3.types.Document.Provenance]): The history of this annotation. """ @@ -881,7 +881,7 @@ class TextAnchor(proto.Message): [Document.text][google.cloud.documentai.v1beta3.Document.text]. Attributes: - text_segments (Sequence[~.document.Document.TextAnchor.TextSegment]): + text_segments (Sequence[google.cloud.documentai_v1beta3.types.Document.TextAnchor.TextSegment]): The text segments from the [Document.text][google.cloud.documentai.v1beta3.Document.text]. content (str): @@ -924,7 +924,7 @@ class PageAnchor(proto.Message): polygons and optionally reference specific layout element types. Attributes: - page_refs (Sequence[~.document.Document.PageAnchor.PageRef]): + page_refs (Sequence[google.cloud.documentai_v1beta3.types.Document.PageAnchor.PageRef]): One or more references to visual page elements """ @@ -938,14 +938,14 @@ class PageRef(proto.Message): Required. Index into the [Document.pages][google.cloud.documentai.v1beta3.Document.pages] element - layout_type (~.document.Document.PageAnchor.PageRef.LayoutType): + layout_type (google.cloud.documentai_v1beta3.types.Document.PageAnchor.PageRef.LayoutType): Optional. The type of the layout element that is being referenced if any. layout_id (str): Optional. Deprecated. Use [PageRef.bounding_poly][google.cloud.documentai.v1beta3.Document.PageAnchor.PageRef.bounding_poly] instead. - bounding_poly (~.geometry.BoundingPoly): + bounding_poly (google.cloud.documentai_v1beta3.types.BoundingPoly): Optional. Identifies the bounding polygon of a layout element on the page. """ @@ -988,10 +988,10 @@ class Provenance(proto.Message): id (int): The Id of this operation. Needs to be unique within the scope of the revision. - parents (Sequence[~.document.Document.Provenance.Parent]): + parents (Sequence[google.cloud.documentai_v1beta3.types.Document.Provenance.Parent]): References to the original elements that are replaced. - type_ (~.document.Document.Provenance.OperationType): + type_ (google.cloud.documentai_v1beta3.types.Document.Provenance.OperationType): The type of provenance operation. """ @@ -1052,9 +1052,9 @@ class Revision(proto.Message): The revisions that this revision is based on. This can include one or more parent (when documents are merged.) This field represents the index into the ``revisions`` field. - create_time (~.timestamp.Timestamp): + create_time (google.protobuf.timestamp_pb2.Timestamp): The time that the revision was created. - human_review (~.document.Document.Revision.HumanReview): + human_review (google.cloud.documentai_v1beta3.types.Document.Revision.HumanReview): Human Review information of this revision. """ @@ -1093,7 +1093,7 @@ class TextChange(proto.Message): r"""This message is used for text changes aka. OCR corrections. Attributes: - text_anchor (~.document.Document.TextAnchor): + text_anchor (google.cloud.documentai_v1beta3.types.Document.TextAnchor): Provenance of the correction. Text anchor indexing into the [Document.text][google.cloud.documentai.v1beta3.Document.text]. There can only be a single ``TextAnchor.text_segments`` @@ -1102,7 +1102,7 @@ class TextChange(proto.Message): changed_text (str): The text that replaces the text identified in the ``text_anchor``. - provenance (Sequence[~.document.Document.Provenance]): + provenance (Sequence[google.cloud.documentai_v1beta3.types.Document.Provenance]): The history of this annotation. """ diff --git a/google/cloud/documentai_v1beta3/types/document_processor_service.py b/google/cloud/documentai_v1beta3/types/document_processor_service.py index 7d235c25..85e71196 100644 --- a/google/cloud/documentai_v1beta3/types/document_processor_service.py +++ b/google/cloud/documentai_v1beta3/types/document_processor_service.py @@ -44,7 +44,7 @@ class ProcessRequest(proto.Message): Attributes: name (str): Required. The processor resource name. - document (~.gcd_document.Document): + document (google.cloud.documentai_v1beta3.types.Document): The document payload, the [content] and [mime_type] fields must be set. skip_human_review (bool): @@ -63,7 +63,7 @@ class ProcessResponse(proto.Message): r"""Response message for the process document method. Attributes: - document (~.gcd_document.Document): + document (google.cloud.documentai_v1beta3.types.Document): The document payload, will populate fields based on the processor's behavior. human_review_operation (str): @@ -86,10 +86,10 @@ class BatchProcessRequest(proto.Message): Attributes: name (str): Required. The processor resource name. - input_configs (Sequence[~.document_processor_service.BatchProcessRequest.BatchInputConfig]): + input_configs (Sequence[google.cloud.documentai_v1beta3.types.BatchProcessRequest.BatchInputConfig]): The input config for each single document in the batch process. - output_config (~.document_processor_service.BatchProcessRequest.BatchOutputConfig): + output_config (google.cloud.documentai_v1beta3.types.BatchProcessRequest.BatchOutputConfig): The overall output config for batch process. """ @@ -139,17 +139,17 @@ class BatchProcessMetadata(proto.Message): r"""The long running operation metadata for batch process method. Attributes: - state (~.document_processor_service.BatchProcessMetadata.State): + state (google.cloud.documentai_v1beta3.types.BatchProcessMetadata.State): The state of the current batch processing. state_message (str): A message providing more details about the current state of processing. For example, the error message if the operation is failed. - create_time (~.timestamp.Timestamp): + create_time (google.protobuf.timestamp_pb2.Timestamp): The creation time of the operation. - update_time (~.timestamp.Timestamp): + update_time (google.protobuf.timestamp_pb2.Timestamp): The last update time of the operation. - individual_process_statuses (Sequence[~.document_processor_service.BatchProcessMetadata.IndividualProcessStatus]): + individual_process_statuses (Sequence[google.cloud.documentai_v1beta3.types.BatchProcessMetadata.IndividualProcessStatus]): The list of response details of each document. """ @@ -175,7 +175,7 @@ class IndividualProcessStatus(proto.Message): batch process is started by take snapshot of that document, since a user can move or change that document during the process. - status (~.gr_status.Status): + status (google.rpc.status_pb2.Status): The status of the processing of the document. output_gcs_destination (str): The output_gcs_destination (in the request as @@ -219,7 +219,7 @@ class ReviewDocumentRequest(proto.Message): Required. The resource name of the HumanReviewConfig that the document will be reviewed with. - document (~.gcd_document.Document): + document (google.cloud.documentai_v1beta3.types.Document): The document that needs human review. """ @@ -245,15 +245,15 @@ class ReviewDocumentOperationMetadata(proto.Message): method. Attributes: - state (~.document_processor_service.ReviewDocumentOperationMetadata.State): + state (google.cloud.documentai_v1beta3.types.ReviewDocumentOperationMetadata.State): Used only when Operation.done is false. state_message (str): A message providing more details about the current state of processing. For example, the error message if the operation is failed. - create_time (~.timestamp.Timestamp): + create_time (google.protobuf.timestamp_pb2.Timestamp): The creation time of the operation. - update_time (~.timestamp.Timestamp): + update_time (google.protobuf.timestamp_pb2.Timestamp): The last update time of the operation. """ diff --git a/google/cloud/documentai_v1beta3/types/geometry.py b/google/cloud/documentai_v1beta3/types/geometry.py index b72dab75..53c3b9b0 100644 --- a/google/cloud/documentai_v1beta3/types/geometry.py +++ b/google/cloud/documentai_v1beta3/types/geometry.py @@ -62,9 +62,9 @@ class BoundingPoly(proto.Message): r"""A bounding polygon for the detected image annotation. Attributes: - vertices (Sequence[~.geometry.Vertex]): + vertices (Sequence[google.cloud.documentai_v1beta3.types.Vertex]): The bounding polygon vertices. - normalized_vertices (Sequence[~.geometry.NormalizedVertex]): + normalized_vertices (Sequence[google.cloud.documentai_v1beta3.types.NormalizedVertex]): The bounding polygon normalized vertices. """ diff --git a/synth.metadata b/synth.metadata index 8d9424c7..a43fa836 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,15 +4,15 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/python-documentai.git", - "sha": "c94afd55124b0abc8978bf86b84743dd4afb0778" + "sha": "33dc25806d5afd147c7cfb4b5f9c5505683b7ec4" } }, { "git": { "name": "googleapis", "remote": "https://github.com/googleapis/googleapis.git", - "sha": "dd372aa22ded7a8ba6f0e03a80e06358a3fa0907", - "internalRef": "347055288" + "sha": "520682435235d9c503983a360a2090025aa47cd1", + "internalRef": "350246057" } }, { @@ -51,6 +51,7 @@ } ], "generatedFiles": [ + ".coveragerc", ".flake8", ".github/CONTRIBUTING.md", ".github/ISSUE_TEMPLATE/bug_report.md", @@ -103,8 +104,10 @@ "docs/_static/custom.css", "docs/_templates/layout.html", "docs/conf.py", + "docs/documentai_v1beta2/document_understanding_service.rst", "docs/documentai_v1beta2/services.rst", "docs/documentai_v1beta2/types.rst", + "docs/documentai_v1beta3/document_processor_service.rst", "docs/documentai_v1beta3/services.rst", "docs/documentai_v1beta3/types.rst", "docs/multiprocessing.rst", diff --git a/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py b/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py index 507a10de..9e708943 100644 --- a/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py +++ b/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py @@ -99,9 +99,22 @@ def test__get_default_mtls_endpoint(): ) +def test_document_understanding_service_client_from_service_account_info(): + creds = credentials.AnonymousCredentials() + with mock.patch.object( + service_account.Credentials, "from_service_account_info" + ) as factory: + factory.return_value = creds + info = {"valid": True} + client = DocumentUnderstandingServiceClient.from_service_account_info(info) + assert client.transport._credentials == creds + + assert client.transport._host == "us-documentai.googleapis.com:443" + + @pytest.mark.parametrize( "client_class", - [DocumentUnderstandingServiceClient, DocumentUnderstandingServiceAsyncClient], + [DocumentUnderstandingServiceClient, DocumentUnderstandingServiceAsyncClient,], ) def test_document_understanding_service_client_from_service_account_file(client_class): creds = credentials.AnonymousCredentials() @@ -120,7 +133,10 @@ def test_document_understanding_service_client_from_service_account_file(client_ def test_document_understanding_service_client_get_transport_class(): transport = DocumentUnderstandingServiceClient.get_transport_class() - assert transport == transports.DocumentUnderstandingServiceGrpcTransport + available_transports = [ + transports.DocumentUnderstandingServiceGrpcTransport, + ] + assert transport in available_transports transport = DocumentUnderstandingServiceClient.get_transport_class("grpc") assert transport == transports.DocumentUnderstandingServiceGrpcTransport @@ -1032,7 +1048,7 @@ def test_document_understanding_service_host_with_port(): def test_document_understanding_service_grpc_transport_channel(): - channel = grpc.insecure_channel("http://localhost/") + channel = grpc.secure_channel("http://localhost/", grpc.local_channel_credentials()) # Check that channel is used if provided. transport = transports.DocumentUnderstandingServiceGrpcTransport( @@ -1044,7 +1060,7 @@ def test_document_understanding_service_grpc_transport_channel(): def test_document_understanding_service_grpc_asyncio_transport_channel(): - channel = aio.insecure_channel("http://localhost/") + channel = aio.secure_channel("http://localhost/", grpc.local_channel_credentials()) # Check that channel is used if provided. transport = transports.DocumentUnderstandingServiceGrpcAsyncIOTransport( @@ -1069,7 +1085,7 @@ def test_document_understanding_service_transport_channel_mtls_with_client_cert_ "grpc.ssl_channel_credentials", autospec=True ) as grpc_ssl_channel_cred: with mock.patch.object( - transport_class, "create_channel", autospec=True + transport_class, "create_channel" ) as grpc_create_channel: mock_ssl_cred = mock.Mock() grpc_ssl_channel_cred.return_value = mock_ssl_cred @@ -1124,7 +1140,7 @@ def test_document_understanding_service_transport_channel_mtls_with_adc( ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), ): with mock.patch.object( - transport_class, "create_channel", autospec=True + transport_class, "create_channel" ) as grpc_create_channel: mock_grpc_channel = mock.Mock() grpc_create_channel.return_value = mock_grpc_channel diff --git a/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py b/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py index bfe8ffe9..abbb8905 100644 --- a/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py +++ b/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py @@ -106,9 +106,22 @@ def test__get_default_mtls_endpoint(): ) +def test_document_processor_service_client_from_service_account_info(): + creds = credentials.AnonymousCredentials() + with mock.patch.object( + service_account.Credentials, "from_service_account_info" + ) as factory: + factory.return_value = creds + info = {"valid": True} + client = DocumentProcessorServiceClient.from_service_account_info(info) + assert client.transport._credentials == creds + + assert client.transport._host == "us-documentai.googleapis.com:443" + + @pytest.mark.parametrize( "client_class", - [DocumentProcessorServiceClient, DocumentProcessorServiceAsyncClient], + [DocumentProcessorServiceClient, DocumentProcessorServiceAsyncClient,], ) def test_document_processor_service_client_from_service_account_file(client_class): creds = credentials.AnonymousCredentials() @@ -127,7 +140,10 @@ def test_document_processor_service_client_from_service_account_file(client_clas def test_document_processor_service_client_get_transport_class(): transport = DocumentProcessorServiceClient.get_transport_class() - assert transport == transports.DocumentProcessorServiceGrpcTransport + available_transports = [ + transports.DocumentProcessorServiceGrpcTransport, + ] + assert transport in available_transports transport = DocumentProcessorServiceClient.get_transport_class("grpc") assert transport == transports.DocumentProcessorServiceGrpcTransport @@ -1306,7 +1322,7 @@ def test_document_processor_service_host_with_port(): def test_document_processor_service_grpc_transport_channel(): - channel = grpc.insecure_channel("http://localhost/") + channel = grpc.secure_channel("http://localhost/", grpc.local_channel_credentials()) # Check that channel is used if provided. transport = transports.DocumentProcessorServiceGrpcTransport( @@ -1318,7 +1334,7 @@ def test_document_processor_service_grpc_transport_channel(): def test_document_processor_service_grpc_asyncio_transport_channel(): - channel = aio.insecure_channel("http://localhost/") + channel = aio.secure_channel("http://localhost/", grpc.local_channel_credentials()) # Check that channel is used if provided. transport = transports.DocumentProcessorServiceGrpcAsyncIOTransport( @@ -1343,7 +1359,7 @@ def test_document_processor_service_transport_channel_mtls_with_client_cert_sour "grpc.ssl_channel_credentials", autospec=True ) as grpc_ssl_channel_cred: with mock.patch.object( - transport_class, "create_channel", autospec=True + transport_class, "create_channel" ) as grpc_create_channel: mock_ssl_cred = mock.Mock() grpc_ssl_channel_cred.return_value = mock_ssl_cred @@ -1396,7 +1412,7 @@ def test_document_processor_service_transport_channel_mtls_with_adc(transport_cl ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), ): with mock.patch.object( - transport_class, "create_channel", autospec=True + transport_class, "create_channel" ) as grpc_create_channel: mock_grpc_channel = mock.Mock() grpc_create_channel.return_value = mock_grpc_channel From 09b34ec3002ca3715c82808ae2c22b747c4602ac Mon Sep 17 00:00:00 2001 From: Mike <45373284+munkhuushmgl@users.noreply.github.com> Date: Tue, 12 Jan 2021 11:19:49 -0800 Subject: [PATCH 16/30] samples: migrate v1beta2 doc AI samples (#79) * samples: migrate v1beta2 doc AI samples * added noxfile * reformatted code * organized imports in right order * lint * finally fixed lint * reorganized folders * imports * added from prefix imports * renamed files * renamed package on tests files * nit --- samples/snippets/batch_parse_form_v1beta2.py | 99 ++++++++++++++++ .../snippets/batch_parse_form_v1beta2_test.py | 46 ++++++++ samples/snippets/batch_parse_table_v1beta2.py | 107 ++++++++++++++++++ .../batch_parse_table_v1beta2_test.py | 46 ++++++++ samples/snippets/noxfile.py | 38 ++++--- samples/snippets/parse_form_v1beta2.py | 92 +++++++++++++++ samples/snippets/parse_form_v1beta2_test.py | 28 +++++ samples/snippets/parse_table_v1beta2.py | 95 ++++++++++++++++ samples/snippets/parse_table_v1beta2_test.py | 28 +++++ samples/snippets/parse_with_model_v1beta2.py | 60 ++++++++++ .../snippets/parse_with_model_v1beta2_test.py | 36 ++++++ samples/snippets/quickstart_v1beta2.py | 65 +++++++++++ samples/snippets/quickstart_v1beta2_test.py | 28 +++++ samples/snippets/set_endpoint_v1beta2.py | 48 ++++++++ samples/snippets/set_endpoint_v1beta2_test.py | 27 +++++ 15 files changed, 825 insertions(+), 18 deletions(-) create mode 100644 samples/snippets/batch_parse_form_v1beta2.py create mode 100644 samples/snippets/batch_parse_form_v1beta2_test.py create mode 100644 samples/snippets/batch_parse_table_v1beta2.py create mode 100644 samples/snippets/batch_parse_table_v1beta2_test.py create mode 100644 samples/snippets/parse_form_v1beta2.py create mode 100644 samples/snippets/parse_form_v1beta2_test.py create mode 100644 samples/snippets/parse_table_v1beta2.py create mode 100644 samples/snippets/parse_table_v1beta2_test.py create mode 100644 samples/snippets/parse_with_model_v1beta2.py create mode 100644 samples/snippets/parse_with_model_v1beta2_test.py create mode 100644 samples/snippets/quickstart_v1beta2.py create mode 100644 samples/snippets/quickstart_v1beta2_test.py create mode 100644 samples/snippets/set_endpoint_v1beta2.py create mode 100644 samples/snippets/set_endpoint_v1beta2_test.py diff --git a/samples/snippets/batch_parse_form_v1beta2.py b/samples/snippets/batch_parse_form_v1beta2.py new file mode 100644 index 00000000..01c19e1e --- /dev/null +++ b/samples/snippets/batch_parse_form_v1beta2.py @@ -0,0 +1,99 @@ +# 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 documentai_batch_parse_form_beta] +import re + +from google.cloud import documentai_v1beta2 as documentai +from google.cloud import storage + + +def batch_parse_form( + project_id="YOUR_PROJECT_ID", + input_uri="gs://cloud-samples-data/documentai/form.pdf", + destination_uri="gs://your-bucket-id/path/to/save/results/", +): + """Parse a form""" + + client = documentai.DocumentUnderstandingServiceClient() + + gcs_source = documentai.types.GcsSource(uri=input_uri) + + # mime_type can be application/pdf, image/tiff, + # and image/gif, or application/json + input_config = documentai.types.InputConfig( + gcs_source=gcs_source, mime_type="application/pdf" + ) + + # where to write results + output_config = documentai.types.OutputConfig( + gcs_destination=documentai.types.GcsDestination(uri=destination_uri), + pages_per_shard=1, # Map one doc page to one output page + ) + + # Improve form parsing results by providing key-value pair hints. + # For each key hint, key is text that is likely to appear in the + # document as a form field name (i.e. "DOB"). + # Value types are optional, but can be one or more of: + # ADDRESS, LOCATION, ORGANIZATION, PERSON, PHONE_NUMBER, ID, + # NUMBER, EMAIL, PRICE, TERMS, DATE, NAME + key_value_pair_hints = [ + documentai.types.KeyValuePairHint( + key="Emergency Contact", value_types=["NAME"] + ), + documentai.types.KeyValuePairHint(key="Referred By"), + ] + + # Setting enabled=True enables form extraction + form_extraction_params = documentai.types.FormExtractionParams( + enabled=True, key_value_pair_hints=key_value_pair_hints + ) + + # Location can be 'us' or 'eu' + parent = "projects/{}/locations/us".format(project_id) + request = documentai.types.ProcessDocumentRequest( + input_config=input_config, + output_config=output_config, + form_extraction_params=form_extraction_params, + ) + + # Add each ProcessDocumentRequest to the batch request + requests = [] + requests.append(request) + + batch_request = documentai.types.BatchProcessDocumentsRequest( + parent=parent, requests=requests + ) + + operation = client.batch_process_documents(batch_request) + + # Wait for the operation to finish + operation.result() + + # Results are written to GCS. Use a regex to find + # output files + match = re.match(r"gs://([^/]+)/(.+)", destination_uri) + output_bucket = match.group(1) + prefix = match.group(2) + + storage_client = storage.client.Client() + bucket = storage_client.get_bucket(output_bucket) + blob_list = list(bucket.list_blobs(prefix=prefix)) + print("Output files:") + for blob in blob_list: + print(blob.name) + + +# [END documentai_batch_parse_form_beta] diff --git a/samples/snippets/batch_parse_form_v1beta2_test.py b/samples/snippets/batch_parse_form_v1beta2_test.py new file mode 100644 index 00000000..50dc845d --- /dev/null +++ b/samples/snippets/batch_parse_form_v1beta2_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 ladnguage governing permissions and +# limitations under the License. + +import os +import uuid + +from google.cloud import storage + +import pytest + +from samples.snippets import batch_parse_form_v1beta2 + + +BUCKET = "document-ai-{}".format(uuid.uuid4()) +OUTPUT_PREFIX = "TEST_OUTPUT_{}".format(uuid.uuid4()) +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] +INPUT_URI = "gs://cloud-samples-data/documentai/invoice.pdf" +BATCH_OUTPUT_URI = "gs://{}/{}/".format(BUCKET, OUTPUT_PREFIX) + + +@pytest.fixture(autouse=True) +def setup_teardown(): + """Create a temporary bucket to store annotation output.""" + storage_client = storage.Client() + bucket = storage_client.create_bucket(BUCKET) + + yield + + bucket.delete(force=True) + + +def test_batch_parse_form(capsys): + batch_parse_form_v1beta2.batch_parse_form(PROJECT_ID, INPUT_URI, BATCH_OUTPUT_URI) + out, _ = capsys.readouterr() + assert "Output files" in out diff --git a/samples/snippets/batch_parse_table_v1beta2.py b/samples/snippets/batch_parse_table_v1beta2.py new file mode 100644 index 00000000..08942080 --- /dev/null +++ b/samples/snippets/batch_parse_table_v1beta2.py @@ -0,0 +1,107 @@ +# 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 documentai_batch_parse_table_beta] +import re + +from google.cloud import documentai_v1beta2 as documentai +from google.cloud import storage + + +def batch_parse_table( + project_id="YOUR_PROJECT_ID", + input_uri="gs://cloud-samples-data/documentai/form.pdf", + destination_uri="gs://your-bucket-id/path/to/save/results/", +): + """Parse a form""" + + client = documentai.DocumentUnderstandingServiceClient() + + gcs_source = documentai.types.GcsSource(uri=input_uri) + + # mime_type can be application/pdf, image/tiff, + # and image/gif, or application/json + input_config = documentai.types.InputConfig( + gcs_source=gcs_source, mime_type="application/pdf" + ) + + # where to write results + output_config = documentai.types.OutputConfig( + gcs_destination=documentai.types.GcsDestination(uri=destination_uri), + pages_per_shard=1, # Map one doc page to one output page + ) + + # Improve table parsing results by providing bounding boxes + # specifying where the box appears in the document (optional) + table_bound_hints = [ + documentai.types.TableBoundHint( + page_number=1, + bounding_box=documentai.types.BoundingPoly( + # Define a polygon around tables to detect + # Each vertice coordinate must be a number between 0 and 1 + normalized_vertices=[ + # Top left + documentai.types.geometry.NormalizedVertex(x=0, y=0), + # Top right + documentai.types.geometry.NormalizedVertex(x=1, y=0), + # Bottom right + documentai.types.geometry.NormalizedVertex(x=1, y=1), + # Bottom left + documentai.types.geometry.NormalizedVertex(x=0, y=1), + ] + ), + ) + ] + + # Setting enabled=True enables form extraction + table_extraction_params = documentai.types.TableExtractionParams( + enabled=True, table_bound_hints=table_bound_hints + ) + + # Location can be 'us' or 'eu' + parent = "projects/{}/locations/us".format(project_id) + request = documentai.types.ProcessDocumentRequest( + input_config=input_config, + output_config=output_config, + table_extraction_params=table_extraction_params, + ) + + requests = [] + requests.append(request) + + batch_request = documentai.types.BatchProcessDocumentsRequest( + parent=parent, requests=requests + ) + + operation = client.batch_process_documents(batch_request) + + # Wait for the operation to finish + operation.result() + + # Results are written to GCS. Use a regex to find + # output files + match = re.match(r"gs://([^/]+)/(.+)", destination_uri) + output_bucket = match.group(1) + prefix = match.group(2) + + storage_client = storage.client.Client() + bucket = storage_client.get_bucket(output_bucket) + blob_list = list(bucket.list_blobs(prefix=prefix)) + print("Output files:") + for blob in blob_list: + print(blob.name) + + +# [END documentai_batch_parse_table_beta] diff --git a/samples/snippets/batch_parse_table_v1beta2_test.py b/samples/snippets/batch_parse_table_v1beta2_test.py new file mode 100644 index 00000000..ed1be2ee --- /dev/null +++ b/samples/snippets/batch_parse_table_v1beta2_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 ladnguage governing permissions and +# limitations under the License. + +import os +import uuid + +from google.cloud import storage + +import pytest + +from samples.snippets import batch_parse_table_v1beta2 + + +BUCKET = "document-ai-{}".format(uuid.uuid4()) +OUTPUT_PREFIX = "TEST_OUTPUT_{}".format(uuid.uuid4()) +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] +INPUT_URI = "gs://cloud-samples-data/documentai/invoice.pdf" +BATCH_OUTPUT_URI = "gs://{}/{}/".format(BUCKET, OUTPUT_PREFIX) + + +@pytest.fixture(autouse=True) +def setup_teardown(): + """Create a temporary bucket to store annotation output.""" + storage_client = storage.Client() + bucket = storage_client.create_bucket(BUCKET) + + yield + + bucket.delete(force=True) + + +def test_batch_parse_table(capsys): + batch_parse_table_v1beta2.batch_parse_table(PROJECT_ID, INPUT_URI, BATCH_OUTPUT_URI) + out, _ = capsys.readouterr() + assert "Output files:" in out diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index bca0522e..bbd25fcd 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -38,28 +38,25 @@ TEST_CONFIG = { # You can opt out from the test for specific Python versions. - 'ignored_versions': ["2.7"], - + "ignored_versions": ["2.7"], # Old samples are opted out of enforcing Python type hints # All new samples should feature them - 'enforce_type_hints': False, - + "enforce_type_hints": False, # 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)) @@ -74,12 +71,12 @@ def get_pytest_env_vars() -> Dict[str, str]: 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 @@ -88,7 +85,7 @@ def get_pytest_env_vars() -> Dict[str, str]: 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]) @@ -137,7 +134,7 @@ def _determine_local_import_names(start_dir: str) -> List[str]: @nox.session def lint(session: nox.sessions.Session) -> None: - if not TEST_CONFIG['enforce_type_hints']: + if not TEST_CONFIG["enforce_type_hints"]: session.install("flake8", "flake8-import-order") else: session.install("flake8", "flake8-import-order", "flake8-annotations") @@ -146,9 +143,11 @@ def lint(session: nox.sessions.Session) -> None: args = FLAKE8_COMMON_ARGS + [ "--application-import-names", ",".join(local_names), - "." + ".", ] session.run("flake8", *args) + + # # Black # @@ -161,6 +160,7 @@ def blacken(session: nox.sessions.Session) -> None: session.run("black", *python_files) + # # Sample Tests # @@ -169,7 +169,9 @@ def blacken(session: nox.sessions.Session) -> None: PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] -def _session_tests(session: nox.sessions.Session, post_install: Callable = None) -> None: +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: """Runs py.test for a particular project.""" if os.path.exists("requirements.txt"): session.install("-r", "requirements.txt") @@ -200,9 +202,9 @@ def py(session: nox.sessions.Session) -> None: 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/parse_form_v1beta2.py b/samples/snippets/parse_form_v1beta2.py new file mode 100644 index 00000000..27c99811 --- /dev/null +++ b/samples/snippets/parse_form_v1beta2.py @@ -0,0 +1,92 @@ +# 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 documentai_parse_form_beta] +from google.cloud import documentai_v1beta2 as documentai + + +def parse_form( + project_id="YOUR_PROJECT_ID", + input_uri="gs://cloud-samples-data/documentai/form.pdf", +): + """Parse a form""" + + client = documentai.DocumentUnderstandingServiceClient() + + gcs_source = documentai.types.GcsSource(uri=input_uri) + + # mime_type can be application/pdf, image/tiff, + # and image/gif, or application/json + input_config = documentai.types.InputConfig( + gcs_source=gcs_source, mime_type="application/pdf" + ) + + # Improve form parsing results by providing key-value pair hints. + # For each key hint, key is text that is likely to appear in the + # document as a form field name (i.e. "DOB"). + # Value types are optional, but can be one or more of: + # ADDRESS, LOCATION, ORGANIZATION, PERSON, PHONE_NUMBER, ID, + # NUMBER, EMAIL, PRICE, TERMS, DATE, NAME + key_value_pair_hints = [ + documentai.types.KeyValuePairHint( + key="Emergency Contact", value_types=["NAME"] + ), + documentai.types.KeyValuePairHint(key="Referred By"), + ] + + # Setting enabled=True enables form extraction + form_extraction_params = documentai.types.FormExtractionParams( + enabled=True, key_value_pair_hints=key_value_pair_hints + ) + + # Location can be 'us' or 'eu' + parent = "projects/{}/locations/us".format(project_id) + request = documentai.types.ProcessDocumentRequest( + parent=parent, + input_config=input_config, + form_extraction_params=form_extraction_params, + ) + + document = client.process_document(request=request) + + def _get_text(el): + """Doc AI identifies form fields by their offsets + in document text. This function converts offsets + to text snippets. + """ + response = "" + # If a text segment spans several lines, it will + # be stored in different text segments. + for segment in el.text_anchor.text_segments: + start_index = segment.start_index + end_index = segment.end_index + response += document.text[start_index:end_index] + return response + + for page in document.pages: + print("Page number: {}".format(page.page_number)) + for form_field in page.form_fields: + print( + "Field Name: {}\tConfidence: {}".format( + _get_text(form_field.field_name), form_field.field_name.confidence + ) + ) + print( + "Field Value: {}\tConfidence: {}".format( + _get_text(form_field.field_value), form_field.field_value.confidence + ) + ) + + +# [END documentai_parse_form_beta] diff --git a/samples/snippets/parse_form_v1beta2_test.py b/samples/snippets/parse_form_v1beta2_test.py new file mode 100644 index 00000000..6987612a --- /dev/null +++ b/samples/snippets/parse_form_v1beta2_test.py @@ -0,0 +1,28 @@ +# 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 ladnguage governing permissions and +# limitations under the License. + +import os + +from samples.snippets import parse_form_v1beta2 + + +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] +INPUT_URI = "gs://cloud-samples-data/documentai/form.pdf" + + +def test_parse_form(capsys): + parse_form_v1beta2.parse_form(PROJECT_ID, INPUT_URI) + out, _ = capsys.readouterr() + assert "Field Name" in out + assert "Field Value" in out diff --git a/samples/snippets/parse_table_v1beta2.py b/samples/snippets/parse_table_v1beta2.py new file mode 100644 index 00000000..ac8f5d11 --- /dev/null +++ b/samples/snippets/parse_table_v1beta2.py @@ -0,0 +1,95 @@ +# 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 documentai_parse_table_beta] +from google.cloud import documentai_v1beta2 as documentai + + +def parse_table( + project_id="YOUR_PROJECT_ID", + input_uri="gs://cloud-samples-data/documentai/invoice.pdf", +): + """Parse a form""" + + client = documentai.DocumentUnderstandingServiceClient() + + gcs_source = documentai.types.GcsSource(uri=input_uri) + + # mime_type can be application/pdf, image/tiff, + # and image/gif, or application/json + input_config = documentai.types.InputConfig( + gcs_source=gcs_source, mime_type="application/pdf" + ) + + # Improve table parsing results by providing bounding boxes + # specifying where the box appears in the document (optional) + table_bound_hints = [ + documentai.types.TableBoundHint( + page_number=1, + bounding_box=documentai.types.BoundingPoly( + # Define a polygon around tables to detect + # Each vertice coordinate must be a number between 0 and 1 + normalized_vertices=[ + # Top left + documentai.types.geometry.NormalizedVertex(x=0, y=0), + # Top right + documentai.types.geometry.NormalizedVertex(x=1, y=0), + # Bottom right + documentai.types.geometry.NormalizedVertex(x=1, y=1), + # Bottom left + documentai.types.geometry.NormalizedVertex(x=0, y=1), + ] + ), + ) + ] + + # Setting enabled=True enables form extraction + table_extraction_params = documentai.types.TableExtractionParams( + enabled=True, table_bound_hints=table_bound_hints + ) + + # Location can be 'us' or 'eu' + parent = "projects/{}/locations/us".format(project_id) + request = documentai.types.ProcessDocumentRequest( + parent=parent, + input_config=input_config, + table_extraction_params=table_extraction_params, + ) + + document = client.process_document(request=request) + + def _get_text(el): + """Convert text offset indexes into text snippets.""" + response = "" + # If a text segment spans several lines, it will + # be stored in different text segments. + for segment in el.text_anchor.text_segments: + start_index = segment.start_index + end_index = segment.end_index + response += document.text[start_index:end_index] + return response + + for page in document.pages: + print("Page number: {}".format(page.page_number)) + for table_num, table in enumerate(page.tables): + print("Table {}: ".format(table_num)) + for row_num, row in enumerate(table.header_rows): + cells = "\t".join([_get_text(cell.layout) for cell in row.cells]) + print("Header Row {}: {}".format(row_num, cells)) + for row_num, row in enumerate(table.body_rows): + cells = "\t".join([_get_text(cell.layout) for cell in row.cells]) + print("Row {}: {}".format(row_num, cells)) + + +# [END documentai_parse_table_beta] diff --git a/samples/snippets/parse_table_v1beta2_test.py b/samples/snippets/parse_table_v1beta2_test.py new file mode 100644 index 00000000..4102c926 --- /dev/null +++ b/samples/snippets/parse_table_v1beta2_test.py @@ -0,0 +1,28 @@ +# 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 ladnguage governing permissions and +# limitations under the License. + +import os + +from samples.snippets import parse_table_v1beta2 + + +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] +INPUT_URI = "gs://cloud-samples-data/documentai/invoice.pdf" + + +def test_parse_table(capsys): + parse_table_v1beta2.parse_table(PROJECT_ID, INPUT_URI) + out, _ = capsys.readouterr() + assert "Table" in out + assert "Header Row" in out diff --git a/samples/snippets/parse_with_model_v1beta2.py b/samples/snippets/parse_with_model_v1beta2.py new file mode 100644 index 00000000..59265c4f --- /dev/null +++ b/samples/snippets/parse_with_model_v1beta2.py @@ -0,0 +1,60 @@ +# 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 documentai_parse_with_model_beta] +from google.cloud import documentai_v1beta2 as documentai + + +def parse_with_model( + project_id="YOUR_PROJECT_ID", + input_uri="gs://cloud-samples-data/documentai/invoice.pdf", + automl_model_name="YOUR_AUTOML_MODEL_NAME", +): + """Process a single document with the Document AI API. + + Args: + project_id: your Google Cloud project id + input_uri: the Cloud Storage URI of your input PDF + automl_model_name: the AutoML model name formatted as: + `projects/[PROJECT_ID]/locations/[LOCATION]/models/[MODEL_ID] + where LOCATION is a Compute Engine region, e.g. `us-central1` + """ + + client = documentai.DocumentUnderstandingServiceClient() + + gcs_source = documentai.types.GcsSource(uri=input_uri) + + # mime_type can be application/pdf, image/tiff, + # and image/gif, or application/json + input_config = documentai.types.InputConfig( + gcs_source=gcs_source, mime_type="application/pdf" + ) + + automl_params = documentai.types.AutoMlParams(model=automl_model_name) + + # Location can be 'us' or 'eu' + parent = "projects/{}/locations/us".format(project_id) + request = documentai.types.ProcessDocumentRequest( + parent=parent, input_config=input_config, automl_params=automl_params + ) + + document = client.process_document(request=request) + + for label in document.labels: + print("Label detected: {}".format(label.name)) + print("Confidence: {}".format(label.confidence)) + + +# [END documentai_parse_with_model_beta] diff --git a/samples/snippets/parse_with_model_v1beta2_test.py b/samples/snippets/parse_with_model_v1beta2_test.py new file mode 100644 index 00000000..4b5d3ca5 --- /dev/null +++ b/samples/snippets/parse_with_model_v1beta2_test.py @@ -0,0 +1,36 @@ +# 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 ladnguage governing permissions and +# limitations under the License. + +import os + +from samples.snippets import parse_with_model_v1beta2 + + +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] +INPUT_URI = "gs://cloud-samples-data/documentai/invoice.pdf" +AUTOML_NL_MODEL_ID = "TCN3472481026502981088" + +if "AUTOML_NL_MODEL_ID" in os.environ: + AUTOML_NL_MODEL_ID = os.environ["AUTOML_NL_MODEL_ID"] + +MODEL_NAME = "projects/{}/locations/us-central1/models/{}".format( + PROJECT_ID, AUTOML_NL_MODEL_ID +) + + +def test_parse_with_model(capsys): + parse_with_model_v1beta2.parse_with_model(PROJECT_ID, INPUT_URI, MODEL_NAME) + out, _ = capsys.readouterr() + assert "Label detected" in out + assert "Confidence" in out diff --git a/samples/snippets/quickstart_v1beta2.py b/samples/snippets/quickstart_v1beta2.py new file mode 100644 index 00000000..34f58820 --- /dev/null +++ b/samples/snippets/quickstart_v1beta2.py @@ -0,0 +1,65 @@ +# 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 documentai_quickstart_beta] +from google.cloud import documentai_v1beta2 as documentai + + +def main( + project_id="YOUR_PROJECT_ID", + input_uri="gs://cloud-samples-data/documentai/invoice.pdf", +): + """Process a single document with the Document AI API, including + text extraction and entity extraction.""" + + client = documentai.DocumentUnderstandingServiceClient() + + gcs_source = documentai.types.GcsSource(uri=input_uri) + + # mime_type can be application/pdf, image/tiff, + # and image/gif, or application/json + input_config = documentai.types.InputConfig( + gcs_source=gcs_source, mime_type="application/pdf" + ) + + # Location can be 'us' or 'eu' + parent = "projects/{}/locations/us".format(project_id) + request = documentai.types.ProcessDocumentRequest( + parent=parent, input_config=input_config + ) + + document = client.process_document(request=request) + + # All text extracted from the document + print("Document Text: {}".format(document.text)) + + def _get_text(el): + """Convert text offset indexes into text snippets.""" + response = "" + # If a text segment spans several lines, it will + # be stored in different text segments. + for segment in el.text_anchor.text_segments: + start_index = segment.start_index + end_index = segment.end_index + response += document.text[start_index:end_index] + return response + + for entity in document.entities: + print("Entity type: {}".format(entity.type_)) + print("Text: {}".format(_get_text(entity))) + print("Mention text: {}\n".format(entity.mention_text)) + + +# [END documentai_quickstart_beta] diff --git a/samples/snippets/quickstart_v1beta2_test.py b/samples/snippets/quickstart_v1beta2_test.py new file mode 100644 index 00000000..1868788d --- /dev/null +++ b/samples/snippets/quickstart_v1beta2_test.py @@ -0,0 +1,28 @@ +# 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 ladnguage governing permissions and +# limitations under the License. + +import os + +from samples.snippets import quickstart_v1beta2 + + +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] +INPUT_URI = "gs://cloud-samples-data/documentai/invoice.pdf" + + +def test_quickstart(capsys): + quickstart_v1beta2.main(PROJECT_ID, INPUT_URI) + out, _ = capsys.readouterr() + assert "Entity type" in out + assert "Mention text" in out diff --git a/samples/snippets/set_endpoint_v1beta2.py b/samples/snippets/set_endpoint_v1beta2.py new file mode 100644 index 00000000..0fa9921b --- /dev/null +++ b/samples/snippets/set_endpoint_v1beta2.py @@ -0,0 +1,48 @@ +# 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. + + +def set_endpoint( + project_id="YOUR_PROJECT_ID", + input_uri="gs://cloud-samples-data/documentai/invoice.pdf", +): + """Process a single document with the Document AI API, including + text extraction and entity extraction.""" + + # [START documentai_set_endpoint_beta] + from google.cloud import documentai_v1beta2 as documentai + + client = documentai.DocumentUnderstandingServiceClient( + client_options={"api_endpoint": "eu-documentai.googleapis.com"} + ) + # [END documentai_set_endpoint_beta] + + gcs_source = documentai.types.GcsSource(uri=input_uri) + + # mime_type can be application/pdf, image/tiff, + # and image/gif, or application/json + input_config = documentai.types.InputConfig( + gcs_source=gcs_source, mime_type="application/pdf" + ) + + # Location can be 'us' or 'eu' + parent = "projects/{}/locations/eu".format(project_id) + request = documentai.types.ProcessDocumentRequest( + parent=parent, input_config=input_config + ) + + document = client.process_document(request=request) + + # All text extracted from the document + print("Document Text: {}".format(document.text)) diff --git a/samples/snippets/set_endpoint_v1beta2_test.py b/samples/snippets/set_endpoint_v1beta2_test.py new file mode 100644 index 00000000..be535a28 --- /dev/null +++ b/samples/snippets/set_endpoint_v1beta2_test.py @@ -0,0 +1,27 @@ +# 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 ladnguage governing permissions and +# limitations under the License. + +import os + +from samples.snippets import set_endpoint_v1beta2 + + +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] +INPUT_URI = "gs://cloud-samples-data/documentai/invoice.pdf" + + +def test_set_endpoint(capsys): + set_endpoint_v1beta2.set_endpoint(PROJECT_ID, INPUT_URI) + out, _ = capsys.readouterr() + assert "Document Text" in out From f2cdc1546a033d9d995ca56c8fa017cd43dec161 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 13 Jan 2021 20:16:03 +0100 Subject: [PATCH 17/30] chore(deps): update dependency google-cloud-storage to v1.35.0 (#78) [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [google-cloud-storage](https://togithub.com/googleapis/python-storage) | minor | `==1.33.0` -> `==1.35.0` | --- ### Release Notes
googleapis/python-storage ### [`v1.35.0`](https://togithub.com/googleapis/python-storage/blob/master/CHANGELOG.md#​1350-httpswwwgithubcomgoogleapispython-storagecomparev1340v1350-2020-12-14) [Compare Source](https://togithub.com/googleapis/python-storage/compare/v1.34.0...v1.35.0) ##### Features - support ConnectionError retries for media operations ([#​342](https://www.github.com/googleapis/python-storage/issues/342)) ([e55b25b](https://www.github.com/googleapis/python-storage/commit/e55b25be1e32f17b17bffe1da99fca5062f180cb)) ### [`v1.34.0`](https://togithub.com/googleapis/python-storage/blob/master/CHANGELOG.md#​1340-httpswwwgithubcomgoogleapispython-storagecomparev1330v1340-2020-12-11) [Compare Source](https://togithub.com/googleapis/python-storage/compare/v1.33.0...v1.34.0) ##### Features - make retry parameter public and added in other methods ([#​331](https://www.github.com/googleapis/python-storage/issues/331)) ([910e34c](https://www.github.com/googleapis/python-storage/commit/910e34c57de5823bc3a04adbd87cbfe27fb41882)) ##### Bug Fixes - avoid triggering global logging config ([#​333](https://www.github.com/googleapis/python-storage/issues/333)) ([602108a](https://www.github.com/googleapis/python-storage/commit/602108a976503271fe0d85c8d7891ce8083aca89)), closes [#​332](https://www.github.com/googleapis/python-storage/issues/332) - fall back to 'charset' of 'content_type' in 'download_as_text' ([#​326](https://www.github.com/googleapis/python-storage/issues/326)) ([63ff233](https://www.github.com/googleapis/python-storage/commit/63ff23387f5873c609490be8e58d69ba34a10a5e)), closes [#​319](https://www.github.com/googleapis/python-storage/issues/319) - fix conditional retry handling of camelCase query params ([#​340](https://www.github.com/googleapis/python-storage/issues/340)) ([4ff6141](https://www.github.com/googleapis/python-storage/commit/4ff614161f6a2654a59706f4f72b5fbb614e70ec)) - retry uploads only conditionally ([#​316](https://www.github.com/googleapis/python-storage/issues/316)) ([547740c](https://www.github.com/googleapis/python-storage/commit/547740c0a898492e76ce5e60dd20c7ddb8a53d1f)) - update 'custom_time' setter to record change ([#​323](https://www.github.com/googleapis/python-storage/issues/323)) ([5174154](https://www.github.com/googleapis/python-storage/commit/5174154fe73bb6581efc3cd32ebe12014ceab306)), closes [#​322](https://www.github.com/googleapis/python-storage/issues/322)
--- ### 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-documentai). --- 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 b0259adc..1ef3b702 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,2 +1,2 @@ google-cloud-documentai==0.3.0 -google-cloud-storage==1.33.0 +google-cloud-storage==1.35.0 From 745bb997a02eefb8a9a4c39b264dfb04b705c106 Mon Sep 17 00:00:00 2001 From: Mike <45373284+munkhuushmgl@users.noreply.github.com> Date: Fri, 22 Jan 2021 10:45:09 -0800 Subject: [PATCH 18/30] chore: added increased timeout on flaky batch request (#84) --- samples/snippets/batch_parse_form_v1beta2.py | 3 ++- samples/snippets/batch_parse_form_v1beta2_test.py | 2 +- samples/snippets/batch_parse_table_v1beta2.py | 3 ++- samples/snippets/batch_parse_table_v1beta2_test.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/samples/snippets/batch_parse_form_v1beta2.py b/samples/snippets/batch_parse_form_v1beta2.py index 01c19e1e..ae60fd63 100644 --- a/samples/snippets/batch_parse_form_v1beta2.py +++ b/samples/snippets/batch_parse_form_v1beta2.py @@ -24,6 +24,7 @@ def batch_parse_form( project_id="YOUR_PROJECT_ID", input_uri="gs://cloud-samples-data/documentai/form.pdf", destination_uri="gs://your-bucket-id/path/to/save/results/", + timeout=90 ): """Parse a form""" @@ -80,7 +81,7 @@ def batch_parse_form( operation = client.batch_process_documents(batch_request) # Wait for the operation to finish - operation.result() + operation.result(timeout) # Results are written to GCS. Use a regex to find # output files diff --git a/samples/snippets/batch_parse_form_v1beta2_test.py b/samples/snippets/batch_parse_form_v1beta2_test.py index 50dc845d..6abd19a2 100644 --- a/samples/snippets/batch_parse_form_v1beta2_test.py +++ b/samples/snippets/batch_parse_form_v1beta2_test.py @@ -41,6 +41,6 @@ def setup_teardown(): def test_batch_parse_form(capsys): - batch_parse_form_v1beta2.batch_parse_form(PROJECT_ID, INPUT_URI, BATCH_OUTPUT_URI) + batch_parse_form_v1beta2.batch_parse_form(PROJECT_ID, INPUT_URI, BATCH_OUTPUT_URI, 120) out, _ = capsys.readouterr() assert "Output files" in out diff --git a/samples/snippets/batch_parse_table_v1beta2.py b/samples/snippets/batch_parse_table_v1beta2.py index 08942080..f62495b4 100644 --- a/samples/snippets/batch_parse_table_v1beta2.py +++ b/samples/snippets/batch_parse_table_v1beta2.py @@ -24,6 +24,7 @@ def batch_parse_table( project_id="YOUR_PROJECT_ID", input_uri="gs://cloud-samples-data/documentai/form.pdf", destination_uri="gs://your-bucket-id/path/to/save/results/", + timeout=90 ): """Parse a form""" @@ -88,7 +89,7 @@ def batch_parse_table( operation = client.batch_process_documents(batch_request) # Wait for the operation to finish - operation.result() + operation.result(timeout) # Results are written to GCS. Use a regex to find # output files diff --git a/samples/snippets/batch_parse_table_v1beta2_test.py b/samples/snippets/batch_parse_table_v1beta2_test.py index ed1be2ee..aa890520 100644 --- a/samples/snippets/batch_parse_table_v1beta2_test.py +++ b/samples/snippets/batch_parse_table_v1beta2_test.py @@ -41,6 +41,6 @@ def setup_teardown(): def test_batch_parse_table(capsys): - batch_parse_table_v1beta2.batch_parse_table(PROJECT_ID, INPUT_URI, BATCH_OUTPUT_URI) + batch_parse_table_v1beta2.batch_parse_table(PROJECT_ID, INPUT_URI, BATCH_OUTPUT_URI, 120) out, _ = capsys.readouterr() assert "Output files:" in out From 8d8fc5a54a1e1024bb7918b81b1ae81afcc4d138 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Thu, 28 Jan 2021 07:53:09 -0800 Subject: [PATCH 19/30] chore: exclude `.nox` directories from linting (#87) The samples tests create `.nox` directories with all dependencies installed. These directories should be excluded from linting. I've tested this change locally, and it significantly speeds up linting on my machine. Source-Author: Tim Swast Source-Date: Tue Dec 22 13:04:04 2020 -0600 Source-Repo: googleapis/synthtool Source-Sha: 373861061648b5fe5e0ac4f8a38b32d639ee93e4 Source-Link: https://github.com/googleapis/synthtool/commit/373861061648b5fe5e0ac4f8a38b32d639ee93e4 --- samples/snippets/noxfile.py | 38 ++++++++++++++++++------------------- synth.metadata | 2 +- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index bbd25fcd..bca0522e 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -38,25 +38,28 @@ TEST_CONFIG = { # You can opt out from the test for specific Python versions. - "ignored_versions": ["2.7"], + 'ignored_versions': ["2.7"], + # Old samples are opted out of enforcing Python type hints # All new samples should feature them - "enforce_type_hints": False, + 'enforce_type_hints': False, + # 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)) @@ -71,12 +74,12 @@ def get_pytest_env_vars() -> Dict[str, str]: 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 @@ -85,7 +88,7 @@ def get_pytest_env_vars() -> Dict[str, str]: 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]) @@ -134,7 +137,7 @@ def _determine_local_import_names(start_dir: str) -> List[str]: @nox.session def lint(session: nox.sessions.Session) -> None: - if not TEST_CONFIG["enforce_type_hints"]: + if not TEST_CONFIG['enforce_type_hints']: session.install("flake8", "flake8-import-order") else: session.install("flake8", "flake8-import-order", "flake8-annotations") @@ -143,11 +146,9 @@ def lint(session: nox.sessions.Session) -> None: args = FLAKE8_COMMON_ARGS + [ "--application-import-names", ",".join(local_names), - ".", + "." ] session.run("flake8", *args) - - # # Black # @@ -160,7 +161,6 @@ def blacken(session: nox.sessions.Session) -> None: session.run("black", *python_files) - # # Sample Tests # @@ -169,9 +169,7 @@ def blacken(session: nox.sessions.Session) -> None: PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] -def _session_tests( - session: nox.sessions.Session, post_install: Callable = None -) -> None: +def _session_tests(session: nox.sessions.Session, post_install: Callable = None) -> None: """Runs py.test for a particular project.""" if os.path.exists("requirements.txt"): session.install("-r", "requirements.txt") @@ -202,9 +200,9 @@ def py(session: nox.sessions.Session) -> None: 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/synth.metadata b/synth.metadata index a43fa836..bc4294cd 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,7 +4,7 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/python-documentai.git", - "sha": "33dc25806d5afd147c7cfb4b5f9c5505683b7ec4" + "sha": "745bb997a02eefb8a9a4c39b264dfb04b705c106" } }, { From 930d2f488c1e2f26f4b9021818a8ca0e911c419e Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 12 Feb 2021 07:25:20 +0100 Subject: [PATCH 20/30] chore(deps): update dependency google-cloud-storage to v1.36.0 (#91) --- 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 1ef3b702..f2c288ea 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,2 +1,2 @@ google-cloud-documentai==0.3.0 -google-cloud-storage==1.35.0 +google-cloud-storage==1.36.0 From 1c413dd248c32dad79eb219c51f562d06d88b1f0 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 24 Feb 2021 05:38:30 +0100 Subject: [PATCH 21/30] chore(deps): update dependency google-cloud-storage to v1.36.1 (#92) --- 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 f2c288ea..06853665 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,2 +1,2 @@ google-cloud-documentai==0.3.0 -google-cloud-storage==1.36.0 +google-cloud-storage==1.36.1 From c2630033ec847a4e6f0e3c75d7454db11db8275a Mon Sep 17 00:00:00 2001 From: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Date: Wed, 3 Mar 2021 09:59:58 -0700 Subject: [PATCH 22/30] chore: require samples checks (#65) Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- .github/sync-repo-settings.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/sync-repo-settings.yaml diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml new file mode 100644 index 00000000..af599353 --- /dev/null +++ b/.github/sync-repo-settings.yaml @@ -0,0 +1,13 @@ +# https://github.com/googleapis/repo-automation-bots/tree/master/packages/sync-repo-settings +# Rules for master branch protection +branchProtectionRules: +# Identifies the protection rule pattern. Name of the branch to be protected. +# Defaults to `master` +- pattern: master + requiredStatusCheckContexts: + - 'Kokoro' + - 'cla/google' + - 'Samples - Lint' + - 'Samples - Python 3.6' + - 'Samples - Python 3.7' + - 'Samples - Python 3.8' From dabe48e8c1439ceb8a50c18aa3c7dca848a9117a Mon Sep 17 00:00:00 2001 From: Eric Schmidt Date: Fri, 5 Mar 2021 10:21:50 -0800 Subject: [PATCH 23/30] fix(samples): swaps 'continue' for 'return' (#93) * fix: swaps 'continue' for 'return' --- .../batch_process_documents_sample_v1beta3.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/samples/snippets/batch_process_documents_sample_v1beta3.py b/samples/snippets/batch_process_documents_sample_v1beta3.py index dae938b2..134b9355 100644 --- a/samples/snippets/batch_process_documents_sample_v1beta3.py +++ b/samples/snippets/batch_process_documents_sample_v1beta3.py @@ -78,28 +78,28 @@ def batch_process_documents( print("Output files:") for i, blob in enumerate(blob_list): - # Download the contents of this blob as a bytes object. - if ".json" not in blob.name: - print(f"skipping non-supported file type {blob.name}") - return - # Only parses JSON files - blob_as_bytes = blob.download_as_bytes() - - document = documentai.types.Document.from_json(blob_as_bytes) - print(f"Fetched file {i + 1}") - - # For a full list of Document object attributes, please reference this page: https://googleapis.dev/python/documentai/latest/_modules/google/cloud/documentai_v1beta3/types/document.html#Document - - # Read the text recognition output from the processor - for page in document.pages: - for form_field in page.form_fields: - field_name = get_text(form_field.field_name, document) - field_value = get_text(form_field.field_value, document) - print("Extracted key value pair:") - print(f"\t{field_name}, {field_value}") - for paragraph in document.pages: - paragraph_text = get_text(paragraph.layout, document) - print(f"Paragraph text:\n{paragraph_text}") + # If JSON file, download the contents of this blob as a bytes object. + if ".json" in blob.name: + blob_as_bytes = blob.download_as_bytes() + + document = documentai.types.Document.from_json(blob_as_bytes) + print(f"Fetched file {i + 1}") + + # For a full list of Document object attributes, please reference this page: + # https://cloud.google.com/document-ai/docs/reference/rpc/google.cloud.documentai.v1beta3#document + + # Read the text recognition output from the processor + for page in document.pages: + for form_field in page.form_fields: + field_name = get_text(form_field.field_name, document) + field_value = get_text(form_field.field_value, document) + print("Extracted key value pair:") + print(f"\t{field_name}, {field_value}") + for paragraph in document.pages: + paragraph_text = get_text(paragraph.layout, document) + print(f"Paragraph text:\n{paragraph_text}") + else: + print(f"Skipping non-supported file type {blob.name}") # Extract shards from the text field From bb639f9470304b9c408143a3e8091a4ca8c54160 Mon Sep 17 00:00:00 2001 From: Eric Schmidt Date: Thu, 11 Mar 2021 10:20:35 -0800 Subject: [PATCH 24/30] fix: adds comment with explicit hostname change (#94) * fix: adds switching code for client_options based upon location --- .../snippets/batch_process_documents_sample_v1beta3.py | 7 ++++++- samples/snippets/process_document_sample_v1beta3.py | 8 ++++++-- samples/snippets/quickstart_sample_v1beta3.py | 8 +++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/samples/snippets/batch_process_documents_sample_v1beta3.py b/samples/snippets/batch_process_documents_sample_v1beta3.py index 134b9355..b1ed3226 100644 --- a/samples/snippets/batch_process_documents_sample_v1beta3.py +++ b/samples/snippets/batch_process_documents_sample_v1beta3.py @@ -38,7 +38,12 @@ def batch_process_documents( timeout: int = 300, ): - client = documentai.DocumentProcessorServiceClient() + # You must set the api_endpoint if you use a location other than 'us', e.g.: + opts = {} + if location == "eu": + opts = {"api_endpoint": "eu-documentai.googleapis.com"} + + client = documentai.DocumentProcessorServiceClient(client_options=opts) destination_uri = f"{gcs_output_uri}/{gcs_output_uri_prefix}/" diff --git a/samples/snippets/process_document_sample_v1beta3.py b/samples/snippets/process_document_sample_v1beta3.py index 5b045708..ab69d073 100644 --- a/samples/snippets/process_document_sample_v1beta3.py +++ b/samples/snippets/process_document_sample_v1beta3.py @@ -27,8 +27,12 @@ def process_document_sample( ): from google.cloud import documentai_v1beta3 as documentai - # Instantiates a client - client = documentai.DocumentProcessorServiceClient() + # You must set the api_endpoint if you use a location other than 'us', e.g.: + opts = {} + if location == "eu": + opts = {"api_endpoint": "eu-documentai.googleapis.com"} + + client = documentai.DocumentProcessorServiceClient(client_options=opts) # The full resource name of the processor, e.g.: # projects/project-id/locations/location/processor/processor-id diff --git a/samples/snippets/quickstart_sample_v1beta3.py b/samples/snippets/quickstart_sample_v1beta3.py index 37d44bb0..884b412c 100644 --- a/samples/snippets/quickstart_sample_v1beta3.py +++ b/samples/snippets/quickstart_sample_v1beta3.py @@ -25,7 +25,13 @@ def quickstart(project_id: str, location: str, processor_id: str, file_path: str): - client = documentai.DocumentProcessorServiceClient() + + # You must set the api_endpoint if you use a location other than 'us', e.g.: + opts = {} + if location == "eu": + opts = {"api_endpoint": "eu-documentai.googleapis.com"} + + client = documentai.DocumentProcessorServiceClient(client_options=opts) # The full resource name of the processor, e.g.: # projects/project-id/locations/location/processor/processor-id From 38a6d55bb77a6d319f9aa4a3c1455a969c604ef6 Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Fri, 12 Mar 2021 09:11:04 -0800 Subject: [PATCH 25/30] chore: upgrade gapic-generator-python to 0.40.5 (#89) PiperOrigin-RevId: 354996675 Source-Author: Google APIs Source-Date: Mon Feb 1 12:11:49 2021 -0800 Source-Repo: googleapis/googleapis Source-Sha: 20712b8fe95001b312f62c6c5f33e3e3ec92cfaf Source-Link: https://github.com/googleapis/googleapis/commit/20712b8fe95001b312f62c6c5f33e3e3ec92cfaf --- .../document_understanding_service/client.py | 18 +- .../transports/grpc.py | 23 ++- .../transports/grpc_asyncio.py | 23 ++- .../document_processor_service/client.py | 18 +- .../transports/grpc.py | 23 ++- .../transports/grpc_asyncio.py | 23 ++- synth.metadata | 6 +- .../test_document_understanding_service.py | 184 +++++++++++------- .../test_document_processor_service.py | 184 +++++++++++------- 9 files changed, 303 insertions(+), 199 deletions(-) diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py index d417e2e0..86e21191 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py @@ -281,21 +281,17 @@ def __init__( util.strtobool(os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false")) ) - ssl_credentials = None + client_cert_source_func = 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 + client_cert_source_func = client_options.client_cert_source else: - creds = SslCredentials() - is_mtls = creds.is_mtls - ssl_credentials = creds.ssl_credentials if is_mtls else None + is_mtls = mtls.has_default_client_cert_source() + client_cert_source_func = ( + mtls.default_client_cert_source() if is_mtls else None + ) # Figure out which api endpoint to use. if client_options.api_endpoint is not None: @@ -338,7 +334,7 @@ def __init__( credentials_file=client_options.credentials_file, host=api_endpoint, scopes=client_options.scopes, - ssl_channel_credentials=ssl_credentials, + client_cert_source_for_mtls=client_cert_source_func, quota_project_id=client_options.quota_project_id, client_info=client_info, ) diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py index 230d39f2..c0c57120 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py @@ -62,6 +62,7 @@ def __init__( api_mtls_endpoint: str = None, client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: Callable[[], Tuple[bytes, bytes]] = None, quota_project_id: Optional[str] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: @@ -92,6 +93,10 @@ def __init__( ``api_mtls_endpoint`` is None. ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials for grpc channel. It is ignored if ``channel`` is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` 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): @@ -108,6 +113,11 @@ def __init__( """ self._ssl_channel_credentials = ssl_channel_credentials + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) + if channel: # Sanity check: Ensure that channel and credentials are not both # provided. @@ -117,11 +127,6 @@ def __init__( self._grpc_channel = channel self._ssl_channel_credentials = None 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 @@ -165,12 +170,18 @@ def __init__( scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id ) + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + # 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, + ssl_credentials=self._ssl_channel_credentials, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, options=[ diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py index 7e78a8cd..7ac8f880 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py @@ -108,6 +108,7 @@ def __init__( api_mtls_endpoint: str = None, client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: Callable[[], Tuple[bytes, bytes]] = None, quota_project_id=None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: @@ -139,6 +140,10 @@ def __init__( ``api_mtls_endpoint`` is None. ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials for grpc channel. It is ignored if ``channel`` is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` 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): @@ -155,6 +160,11 @@ def __init__( """ self._ssl_channel_credentials = ssl_channel_credentials + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) + if channel: # Sanity check: Ensure that channel and credentials are not both # provided. @@ -164,11 +174,6 @@ def __init__( self._grpc_channel = channel self._ssl_channel_credentials = None 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 @@ -212,12 +217,18 @@ def __init__( scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id ) + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + # 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, + ssl_credentials=self._ssl_channel_credentials, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, options=[ diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/client.py b/google/cloud/documentai_v1beta3/services/document_processor_service/client.py index 3c039826..9064c7d5 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/client.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/client.py @@ -310,21 +310,17 @@ def __init__( util.strtobool(os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false")) ) - ssl_credentials = None + client_cert_source_func = 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 + client_cert_source_func = client_options.client_cert_source else: - creds = SslCredentials() - is_mtls = creds.is_mtls - ssl_credentials = creds.ssl_credentials if is_mtls else None + is_mtls = mtls.has_default_client_cert_source() + client_cert_source_func = ( + mtls.default_client_cert_source() if is_mtls else None + ) # Figure out which api endpoint to use. if client_options.api_endpoint is not None: @@ -367,7 +363,7 @@ def __init__( credentials_file=client_options.credentials_file, host=api_endpoint, scopes=client_options.scopes, - ssl_channel_credentials=ssl_credentials, + client_cert_source_for_mtls=client_cert_source_func, quota_project_id=client_options.quota_project_id, client_info=client_info, ) diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py index 32bf1e00..9c4681da 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py @@ -63,6 +63,7 @@ def __init__( api_mtls_endpoint: str = None, client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: Callable[[], Tuple[bytes, bytes]] = None, quota_project_id: Optional[str] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: @@ -93,6 +94,10 @@ def __init__( ``api_mtls_endpoint`` is None. ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials for grpc channel. It is ignored if ``channel`` is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` 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): @@ -109,6 +114,11 @@ def __init__( """ self._ssl_channel_credentials = ssl_channel_credentials + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) + if channel: # Sanity check: Ensure that channel and credentials are not both # provided. @@ -118,11 +128,6 @@ def __init__( self._grpc_channel = channel self._ssl_channel_credentials = None 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 @@ -166,12 +171,18 @@ def __init__( scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id ) + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + # 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, + ssl_credentials=self._ssl_channel_credentials, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, options=[ diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py index 5e3b676d..9f46b1c8 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py @@ -107,6 +107,7 @@ def __init__( api_mtls_endpoint: str = None, client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: Callable[[], Tuple[bytes, bytes]] = None, quota_project_id=None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: @@ -138,6 +139,10 @@ def __init__( ``api_mtls_endpoint`` is None. ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials for grpc channel. It is ignored if ``channel`` is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` 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): @@ -154,6 +159,11 @@ def __init__( """ self._ssl_channel_credentials = ssl_channel_credentials + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) + if channel: # Sanity check: Ensure that channel and credentials are not both # provided. @@ -163,11 +173,6 @@ def __init__( self._grpc_channel = channel self._ssl_channel_credentials = None 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 @@ -211,12 +216,18 @@ def __init__( scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id ) + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + # 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, + ssl_credentials=self._ssl_channel_credentials, scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, options=[ diff --git a/synth.metadata b/synth.metadata index bc4294cd..87576957 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,15 +4,15 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/python-documentai.git", - "sha": "745bb997a02eefb8a9a4c39b264dfb04b705c106" + "sha": "8d8fc5a54a1e1024bb7918b81b1ae81afcc4d138" } }, { "git": { "name": "googleapis", "remote": "https://github.com/googleapis/googleapis.git", - "sha": "520682435235d9c503983a360a2090025aa47cd1", - "internalRef": "350246057" + "sha": "20712b8fe95001b312f62c6c5f33e3e3ec92cfaf", + "internalRef": "354996675" } }, { diff --git a/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py b/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py index 9e708943..3c617faa 100644 --- a/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py +++ b/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py @@ -195,7 +195,7 @@ def test_document_understanding_service_client_client_options( credentials_file=None, host="squid.clam.whelk", scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -211,7 +211,7 @@ def test_document_understanding_service_client_client_options( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -227,7 +227,7 @@ def test_document_understanding_service_client_client_options( credentials_file=None, host=client.DEFAULT_MTLS_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -255,7 +255,7 @@ def test_document_understanding_service_client_client_options( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id="octopus", client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -316,29 +316,25 @@ def test_document_understanding_service_client_mtls_env_auto( client_cert_source=client_cert_source_callback ) with mock.patch.object(transport_class, "__init__") as patched: - ssl_channel_creds = mock.Mock() - with mock.patch( - "grpc.ssl_channel_credentials", return_value=ssl_channel_creds - ): - patched.return_value = None - client = client_class(client_options=options) + patched.return_value = None + 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 + if use_client_cert_env == "false": + expected_client_cert_source = None + expected_host = client.DEFAULT_ENDPOINT + else: + expected_client_cert_source = client_cert_source_callback + expected_host = client.DEFAULT_MTLS_ENDPOINT - 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, - ) + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=expected_host, + scopes=None, + client_cert_source_for_mtls=expected_client_cert_source, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) # Check the case ADC client cert is provided. Whether client cert is used depends on # GOOGLE_API_USE_CLIENT_CERTIFICATE value. @@ -347,66 +343,53 @@ def test_document_understanding_service_client_mtls_env_auto( ): with mock.patch.object(transport_class, "__init__") as patched: with mock.patch( - "google.auth.transport.grpc.SslCredentials.__init__", return_value=None + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, ): 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, - ) + "google.auth.transport.mtls.default_client_cert_source", + return_value=client_cert_source_callback, + ): + if use_client_cert_env == "false": + expected_host = client.DEFAULT_ENDPOINT + expected_client_cert_source = None + else: + expected_host = client.DEFAULT_MTLS_ENDPOINT + expected_client_cert_source = client_cert_source_callback - # 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, + host=expected_host, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=expected_client_cert_source, 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.mtls.has_default_client_cert_source", + 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, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) + @pytest.mark.parametrize( "client_class,transport_class,transport_name", @@ -436,7 +419,7 @@ def test_document_understanding_service_client_client_options_scopes( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=["1", "2"], - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -470,7 +453,7 @@ def test_document_understanding_service_client_client_options_credentials_file( credentials_file="credentials.json", host=client.DEFAULT_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -489,7 +472,7 @@ def test_document_understanding_service_client_client_options_from_dict(): credentials_file=None, host="squid.clam.whelk", scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -1027,6 +1010,53 @@ def test_document_understanding_service_transport_auth_adc(): ) +@pytest.mark.parametrize( + "transport_class", + [ + transports.DocumentUnderstandingServiceGrpcTransport, + transports.DocumentUnderstandingServiceGrpcAsyncIOTransport, + ], +) +def test_document_understanding_service_grpc_transport_client_cert_source_for_mtls( + transport_class, +): + cred = credentials.AnonymousCredentials() + + # Check ssl_channel_credentials is used if provided. + with mock.patch.object(transport_class, "create_channel") as mock_create_channel: + mock_ssl_channel_creds = mock.Mock() + transport_class( + host="squid.clam.whelk", + credentials=cred, + ssl_channel_credentials=mock_ssl_channel_creds, + ) + mock_create_channel.assert_called_once_with( + "squid.clam.whelk:443", + credentials=cred, + credentials_file=None, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ssl_credentials=mock_ssl_channel_creds, + quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + # Check if ssl_channel_credentials is not provided, then client_cert_source_for_mtls + # is used. + with mock.patch.object(transport_class, "create_channel", return_value=mock.Mock()): + with mock.patch("grpc.ssl_channel_credentials") as mock_ssl_cred: + transport_class( + credentials=cred, + client_cert_source_for_mtls=client_cert_source_callback, + ) + expected_cert, expected_key = client_cert_source_callback() + mock_ssl_cred.assert_called_once_with( + certificate_chain=expected_cert, private_key=expected_key + ) + + def test_document_understanding_service_host_no_port(): client = DocumentUnderstandingServiceClient( credentials=credentials.AnonymousCredentials(), @@ -1071,6 +1101,8 @@ def test_document_understanding_service_grpc_asyncio_transport_channel(): assert transport._ssl_channel_credentials == None +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. @pytest.mark.parametrize( "transport_class", [ @@ -1123,6 +1155,8 @@ def test_document_understanding_service_transport_channel_mtls_with_client_cert_ assert transport._ssl_channel_credentials == mock_ssl_cred +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. @pytest.mark.parametrize( "transport_class", [ diff --git a/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py b/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py index abbb8905..68a00a85 100644 --- a/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py +++ b/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py @@ -202,7 +202,7 @@ def test_document_processor_service_client_client_options( credentials_file=None, host="squid.clam.whelk", scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -218,7 +218,7 @@ def test_document_processor_service_client_client_options( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -234,7 +234,7 @@ def test_document_processor_service_client_client_options( credentials_file=None, host=client.DEFAULT_MTLS_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -262,7 +262,7 @@ def test_document_processor_service_client_client_options( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id="octopus", client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -323,29 +323,25 @@ def test_document_processor_service_client_mtls_env_auto( client_cert_source=client_cert_source_callback ) with mock.patch.object(transport_class, "__init__") as patched: - ssl_channel_creds = mock.Mock() - with mock.patch( - "grpc.ssl_channel_credentials", return_value=ssl_channel_creds - ): - patched.return_value = None - client = client_class(client_options=options) + patched.return_value = None + 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 + if use_client_cert_env == "false": + expected_client_cert_source = None + expected_host = client.DEFAULT_ENDPOINT + else: + expected_client_cert_source = client_cert_source_callback + expected_host = client.DEFAULT_MTLS_ENDPOINT - 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, - ) + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=expected_host, + scopes=None, + client_cert_source_for_mtls=expected_client_cert_source, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) # Check the case ADC client cert is provided. Whether client cert is used depends on # GOOGLE_API_USE_CLIENT_CERTIFICATE value. @@ -354,66 +350,53 @@ def test_document_processor_service_client_mtls_env_auto( ): with mock.patch.object(transport_class, "__init__") as patched: with mock.patch( - "google.auth.transport.grpc.SslCredentials.__init__", return_value=None + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, ): 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, - ) + "google.auth.transport.mtls.default_client_cert_source", + return_value=client_cert_source_callback, + ): + if use_client_cert_env == "false": + expected_host = client.DEFAULT_ENDPOINT + expected_client_cert_source = None + else: + expected_host = client.DEFAULT_MTLS_ENDPOINT + expected_client_cert_source = client_cert_source_callback - # 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, + host=expected_host, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=expected_client_cert_source, 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.mtls.has_default_client_cert_source", + 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, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) + @pytest.mark.parametrize( "client_class,transport_class,transport_name", @@ -443,7 +426,7 @@ def test_document_processor_service_client_client_options_scopes( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=["1", "2"], - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -477,7 +460,7 @@ def test_document_processor_service_client_client_options_credentials_file( credentials_file="credentials.json", host=client.DEFAULT_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -496,7 +479,7 @@ def test_document_processor_service_client_client_options_from_dict(): credentials_file=None, host="squid.clam.whelk", scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -1301,6 +1284,53 @@ def test_document_processor_service_transport_auth_adc(): ) +@pytest.mark.parametrize( + "transport_class", + [ + transports.DocumentProcessorServiceGrpcTransport, + transports.DocumentProcessorServiceGrpcAsyncIOTransport, + ], +) +def test_document_processor_service_grpc_transport_client_cert_source_for_mtls( + transport_class, +): + cred = credentials.AnonymousCredentials() + + # Check ssl_channel_credentials is used if provided. + with mock.patch.object(transport_class, "create_channel") as mock_create_channel: + mock_ssl_channel_creds = mock.Mock() + transport_class( + host="squid.clam.whelk", + credentials=cred, + ssl_channel_credentials=mock_ssl_channel_creds, + ) + mock_create_channel.assert_called_once_with( + "squid.clam.whelk:443", + credentials=cred, + credentials_file=None, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ssl_credentials=mock_ssl_channel_creds, + quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + # Check if ssl_channel_credentials is not provided, then client_cert_source_for_mtls + # is used. + with mock.patch.object(transport_class, "create_channel", return_value=mock.Mock()): + with mock.patch("grpc.ssl_channel_credentials") as mock_ssl_cred: + transport_class( + credentials=cred, + client_cert_source_for_mtls=client_cert_source_callback, + ) + expected_cert, expected_key = client_cert_source_callback() + mock_ssl_cred.assert_called_once_with( + certificate_chain=expected_cert, private_key=expected_key + ) + + def test_document_processor_service_host_no_port(): client = DocumentProcessorServiceClient( credentials=credentials.AnonymousCredentials(), @@ -1345,6 +1375,8 @@ def test_document_processor_service_grpc_asyncio_transport_channel(): assert transport._ssl_channel_credentials == None +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. @pytest.mark.parametrize( "transport_class", [ @@ -1397,6 +1429,8 @@ def test_document_processor_service_transport_channel_mtls_with_client_cert_sour assert transport._ssl_channel_credentials == mock_ssl_cred +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. @pytest.mark.parametrize( "transport_class", [ From dae4fb5e9cb890fe55915a3c7add1431b7775bba Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 12 Mar 2021 18:11:18 +0100 Subject: [PATCH 26/30] chore(deps): update dependency google-cloud-storage to v1.36.2 (#95) --- 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 06853665..ce6670de 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,2 +1,2 @@ google-cloud-documentai==0.3.0 -google-cloud-storage==1.36.1 +google-cloud-storage==1.36.2 From b78db75d0f3c68a38fbc5e540d264f11f5abfd3b Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Fri, 12 Mar 2021 09:26:02 -0800 Subject: [PATCH 27/30] chore: update templates (#82) This PR was generated using Autosynth. :rainbow: Synth log will be available here: https://source.cloud.google.com/results/invocations/008ce5bf-87d6-4df6-a516-9e6d6ecebea4/targets - [ ] To automatically regenerate this PR, check this box. Source-Link: https://github.com/googleapis/synthtool/commit/41a4e56982620d3edcf110d76f4fcdfdec471ac8 Source-Link: https://github.com/googleapis/synthtool/commit/f15b57ccfd71106c2299e9b89835fe6e55015662 --- LICENSE | 7 ++++--- docs/_static/custom.css | 7 ++++++- synth.metadata | 6 +++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/LICENSE b/LICENSE index a8ee855d..d6456956 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ - Apache License + + Apache License Version 2.0, January 2004 - https://www.apache.org/licenses/ + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -192,7 +193,7 @@ 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, diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 0abaf229..bcd37bbd 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,4 +1,9 @@ div#python2-eol { border-color: red; border-width: medium; -} \ No newline at end of file +} + +/* Ensure minimum width for 'Parameters' / 'Returns' column */ +dl.field-list > dt { + min-width: 100px +} diff --git a/synth.metadata b/synth.metadata index 87576957..6117686d 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,7 +4,7 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/python-documentai.git", - "sha": "8d8fc5a54a1e1024bb7918b81b1ae81afcc4d138" + "sha": "d6f183a696b211c6d29bc28e9bbd0a8537f65577" } }, { @@ -19,14 +19,14 @@ "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "373861061648b5fe5e0ac4f8a38b32d639ee93e4" + "sha": "41a4e56982620d3edcf110d76f4fcdfdec471ac8" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "373861061648b5fe5e0ac4f8a38b32d639ee93e4" + "sha": "41a4e56982620d3edcf110d76f4fcdfdec471ac8" } } ], From 9fd02a6b9ba34a6762a762f12de9948daf1ea9bb Mon Sep 17 00:00:00 2001 From: Yoshi Automation Bot Date: Tue, 23 Mar 2021 12:45:47 -0700 Subject: [PATCH 28/30] chore: update templates (#97) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(python): skip docfx in main presubmit * fix: properly template the repo name Source-Author: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Source-Date: Fri Jan 8 10:32:13 2021 -0700 Source-Repo: googleapis/synthtool Source-Sha: fb53b6fb373b7c3edf4e55f3e8036bc6d73fa483 Source-Link: https://github.com/googleapis/synthtool/commit/fb53b6fb373b7c3edf4e55f3e8036bc6d73fa483 * chore: add missing quotation mark Source-Author: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Source-Date: Mon Jan 11 09:43:06 2021 -0700 Source-Repo: googleapis/synthtool Source-Sha: 16ec872dd898d7de6e1822badfac32484b5d9031 Source-Link: https://github.com/googleapis/synthtool/commit/16ec872dd898d7de6e1822badfac32484b5d9031 * chore: add 3.9 to noxfile template Since the python-docs-samples noxfile-template doesn't sync with this, I wanted to make sure the noxfile template matched the most recent change [here](https://github.com/GoogleCloudPlatform/python-docs-samples/pull/4968/files) cc @tmatsuo Source-Author: Leah E. Cole <6719667+leahecole@users.noreply.github.com> Source-Date: Fri Jan 15 17:24:05 2021 -0800 Source-Repo: googleapis/synthtool Source-Sha: 56ddc68f36b32341e9f22c2c59b4ce6aa3ba635f Source-Link: https://github.com/googleapis/synthtool/commit/56ddc68f36b32341e9f22c2c59b4ce6aa3ba635f * build(python): make `NOX_SESSION` optional I added this accidentally in #889. `NOX_SESSION` should be passed down if it is set but not marked required. Source-Author: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Source-Date: Tue Jan 19 09:38:04 2021 -0700 Source-Repo: googleapis/synthtool Source-Sha: ba960d730416fe05c50547e975ce79fcee52c671 Source-Link: https://github.com/googleapis/synthtool/commit/ba960d730416fe05c50547e975ce79fcee52c671 * chore: Add header checker config to python library synth Now that we have it working in [python-docs-samples](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/.github/header-checker-lint.yml) we should consider adding it to the 🐍 libraries :) Source-Author: Leah E. Cole <6719667+leahecole@users.noreply.github.com> Source-Date: Mon Jan 25 13:24:08 2021 -0800 Source-Repo: googleapis/synthtool Source-Sha: 573f7655311b553a937f9123bee17bf78497db95 Source-Link: https://github.com/googleapis/synthtool/commit/573f7655311b553a937f9123bee17bf78497db95 * chore: add noxfile parameters for extra dependencies Also, add tests for some noxfile parameters for assurance that the template generates valid Python. Co-authored-by: Jeffrey Rennie Source-Author: Tim Swast Source-Date: Tue Jan 26 12:26:57 2021 -0600 Source-Repo: googleapis/synthtool Source-Sha: 778d8beae28d6d87eb01fdc839a4b4d966ed2ebe Source-Link: https://github.com/googleapis/synthtool/commit/778d8beae28d6d87eb01fdc839a4b4d966ed2ebe * build: migrate to flakybot Source-Author: Justin Beckwith Source-Date: Thu Jan 28 22:22:38 2021 -0800 Source-Repo: googleapis/synthtool Source-Sha: d1bb9173100f62c0cfc8f3138b62241e7f47ca6a Source-Link: https://github.com/googleapis/synthtool/commit/d1bb9173100f62c0cfc8f3138b62241e7f47ca6a * chore(python): include py.typed files in release A py.typed file must be included in the released package for it to be considered typed by type checkers. https://www.python.org/dev/peps/pep-0561/#packaging-type-information. See https://github.com/googleapis/python-secret-manager/issues/79 Source-Author: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Source-Date: Fri Feb 5 17:32:06 2021 -0700 Source-Repo: googleapis/synthtool Source-Sha: 33366574ffb9e11737b3547eb6f020ecae0536e8 Source-Link: https://github.com/googleapis/synthtool/commit/33366574ffb9e11737b3547eb6f020ecae0536e8 * docs: update python contributing guide Adds details about blacken, updates version for system tests, and shows how to pass through pytest arguments. Source-Author: Chris Cotter Source-Date: Mon Feb 8 17:13:36 2021 -0500 Source-Repo: googleapis/synthtool Source-Sha: 4679e7e415221f03ff2a71e3ffad75b9ec41d87e Source-Link: https://github.com/googleapis/synthtool/commit/4679e7e415221f03ff2a71e3ffad75b9ec41d87e * build(python): enable flakybot on library unit and system tests Source-Author: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Source-Date: Wed Feb 17 14:10:46 2021 -0700 Source-Repo: googleapis/synthtool Source-Sha: d17674372e27fb8f23013935e794aa37502071aa Source-Link: https://github.com/googleapis/synthtool/commit/d17674372e27fb8f23013935e794aa37502071aa * test: install pyopenssl for mtls testing Source-Author: arithmetic1728 <58957152+arithmetic1728@users.noreply.github.com> Source-Date: Tue Mar 2 12:27:56 2021 -0800 Source-Repo: googleapis/synthtool Source-Sha: 0780323da96d5a53925fe0547757181fe76e8f1e Source-Link: https://github.com/googleapis/synthtool/commit/0780323da96d5a53925fe0547757181fe76e8f1e --- .github/header-checker-lint.yml | 15 +++++++++++++++ .gitignore | 4 +++- .kokoro/build.sh | 26 ++++++++++++++++++++------ .kokoro/docs/docs-presubmit.cfg | 11 +++++++++++ .kokoro/test-samples.sh | 8 ++++---- .kokoro/trampoline_v2.sh | 2 +- .trampolinerc | 1 + CONTRIBUTING.rst | 22 ++++++++++++++++++---- MANIFEST.in | 4 ++-- noxfile.py | 32 ++++++++++++++++++++++++++++++-- samples/snippets/noxfile.py | 2 +- synth.metadata | 7 ++++--- 12 files changed, 110 insertions(+), 24 deletions(-) create mode 100644 .github/header-checker-lint.yml diff --git a/.github/header-checker-lint.yml b/.github/header-checker-lint.yml new file mode 100644 index 00000000..fc281c05 --- /dev/null +++ b/.github/header-checker-lint.yml @@ -0,0 +1,15 @@ +{"allowedCopyrightHolders": ["Google LLC"], + "allowedLicenses": ["Apache-2.0", "MIT", "BSD-3"], + "ignoreFiles": ["**/requirements.txt", "**/requirements-test.txt"], + "sourceFileExtensions": [ + "ts", + "js", + "java", + "sh", + "Dockerfile", + "yaml", + "py", + "html", + "txt" + ] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index b9daa52f..b4243ced 100644 --- a/.gitignore +++ b/.gitignore @@ -50,8 +50,10 @@ docs.metadata # Virtual environment env/ + +# Test logs coverage.xml -sponge_log.xml +*sponge_log.xml # System test environment variables. system_tests/local_test_setup diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 4eb2a8df..83e6a9e7 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -15,7 +15,11 @@ set -eo pipefail -cd github/python-documentai +if [[ -z "${PROJECT_ROOT:-}" ]]; then + PROJECT_ROOT="github/python-documentai" +fi + +cd "${PROJECT_ROOT}" # Disable buffering, so that the logs stream through. export PYTHONUNBUFFERED=1 @@ -30,16 +34,26 @@ export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/service-account.json export PROJECT_ID=$(cat "${KOKORO_GFILE_DIR}/project-id.json") # Remove old nox -python3.6 -m pip uninstall --yes --quiet nox-automation +python3 -m pip uninstall --yes --quiet nox-automation # Install nox -python3.6 -m pip install --upgrade --quiet nox -python3.6 -m nox --version +python3 -m pip install --upgrade --quiet nox +python3 -m nox --version + +# If this is a continuous build, send the test log to the FlakyBot. +# See https://github.com/googleapis/repo-automation-bots/tree/master/packages/flakybot. +if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]]; then + cleanup() { + chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot + $KOKORO_GFILE_DIR/linux_amd64/flakybot + } + trap cleanup EXIT HUP +fi # If NOX_SESSION is set, it only runs the specified session, # otherwise run all the sessions. if [[ -n "${NOX_SESSION:-}" ]]; then - python3.6 -m nox -s "${NOX_SESSION:-}" + python3 -m nox -s ${NOX_SESSION:-} else - python3.6 -m nox + python3 -m nox fi diff --git a/.kokoro/docs/docs-presubmit.cfg b/.kokoro/docs/docs-presubmit.cfg index 11181078..c1cf9b5a 100644 --- a/.kokoro/docs/docs-presubmit.cfg +++ b/.kokoro/docs/docs-presubmit.cfg @@ -15,3 +15,14 @@ env_vars: { key: "TRAMPOLINE_IMAGE_UPLOAD" value: "false" } + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-documentai/.kokoro/build.sh" +} + +# Only run this nox session. +env_vars: { + key: "NOX_SESSION" + value: "docs docfx" +} diff --git a/.kokoro/test-samples.sh b/.kokoro/test-samples.sh index f97019c6..eca0482e 100755 --- a/.kokoro/test-samples.sh +++ b/.kokoro/test-samples.sh @@ -87,11 +87,11 @@ for file in samples/**/requirements.txt; do python3.6 -m nox -s "$RUN_TESTS_SESSION" EXIT=$? - # If this is a periodic build, send the test log to the Build Cop Bot. - # See https://github.com/googleapis/repo-automation-bots/tree/master/packages/buildcop. + # If this is a periodic build, send the test log to the FlakyBot. + # See https://github.com/googleapis/repo-automation-bots/tree/master/packages/flakybot. if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"periodic"* ]]; then - chmod +x $KOKORO_GFILE_DIR/linux_amd64/buildcop - $KOKORO_GFILE_DIR/linux_amd64/buildcop + chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot + $KOKORO_GFILE_DIR/linux_amd64/flakybot fi if [[ $EXIT -ne 0 ]]; then diff --git a/.kokoro/trampoline_v2.sh b/.kokoro/trampoline_v2.sh index 719bcd5b..4af6cdc2 100755 --- a/.kokoro/trampoline_v2.sh +++ b/.kokoro/trampoline_v2.sh @@ -159,7 +159,7 @@ if [[ -n "${KOKORO_BUILD_ID:-}" ]]; then "KOKORO_GITHUB_COMMIT" "KOKORO_GITHUB_PULL_REQUEST_NUMBER" "KOKORO_GITHUB_PULL_REQUEST_COMMIT" - # For Build Cop Bot + # For FlakyBot "KOKORO_GITHUB_COMMIT_URL" "KOKORO_GITHUB_PULL_REQUEST_URL" ) diff --git a/.trampolinerc b/.trampolinerc index 995ee291..383b6ec8 100644 --- a/.trampolinerc +++ b/.trampolinerc @@ -24,6 +24,7 @@ required_envvars+=( pass_down_envvars+=( "STAGING_BUCKET" "V2_STAGING_BUCKET" + "NOX_SESSION" ) # Prevent unintentional override on the default image. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index e7cc1eaf..7307ccad 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -70,9 +70,14 @@ We use `nox `__ to instrument our tests. - To test your changes, run unit tests with ``nox``:: $ nox -s unit-2.7 - $ nox -s unit-3.7 + $ nox -s unit-3.8 $ ... +- Args to pytest can be passed through the nox command separated by a `--`. For + example, to run a single test:: + + $ nox -s unit-3.8 -- -k + .. note:: The unit tests and system tests are described in the @@ -93,8 +98,12 @@ On Debian/Ubuntu:: ************ Coding Style ************ +- We use the automatic code formatter ``black``. You can run it using + the nox session ``blacken``. This will eliminate many lint errors. Run via:: + + $ nox -s blacken -- PEP8 compliance, with exceptions defined in the linter configuration. +- PEP8 compliance is required, with exceptions defined in the linter configuration. If you have ``nox`` installed, you can test that you have not introduced any non-compliant code via:: @@ -133,13 +142,18 @@ Running System Tests - To run system tests, you can execute:: - $ nox -s system-3.7 + # Run all system tests + $ nox -s system-3.8 $ nox -s system-2.7 + # Run a single system test + $ nox -s system-3.8 -- -k + + .. note:: System tests are only configured to run under Python 2.7 and - Python 3.7. For expediency, we do not run them in older versions + Python 3.8. For expediency, we do not run them in older versions of Python 3. This alone will not run the tests. You'll need to change some local diff --git a/MANIFEST.in b/MANIFEST.in index e9e29d12..e783f4c6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -16,10 +16,10 @@ # Generated by synthtool. DO NOT EDIT! include README.rst LICENSE -recursive-include google *.json *.proto +recursive-include google *.json *.proto py.typed recursive-include tests * global-exclude *.py[co] global-exclude __pycache__ # Exclude scripts for samples readmegen -prune scripts/readme-gen \ No newline at end of file +prune scripts/readme-gen diff --git a/noxfile.py b/noxfile.py index 8004482e..8d9724d0 100644 --- a/noxfile.py +++ b/noxfile.py @@ -30,6 +30,17 @@ SYSTEM_TEST_PYTHON_VERSIONS = ["3.8"] UNIT_TEST_PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9"] +# 'docfx' is excluded since it only needs to run in 'docs-presubmit' +nox.options.sessions = [ + "unit", + "system", + "cover", + "lint", + "lint_setup_py", + "blacken", + "docs", +] + @nox.session(python=DEFAULT_PYTHON_VERSION) def lint(session): @@ -75,12 +86,14 @@ def default(session): session.install( "mock", "pytest", "pytest-cov", ) + session.install("-e", ".") # Run py.test against the unit tests. session.run( "py.test", "--quiet", + f"--junitxml=unit_{session.python}_sponge_log.xml", "--cov=google/cloud", "--cov=tests/unit", "--cov-append", @@ -110,6 +123,9 @@ def system(session): # Sanity check: Only run tests if the environment variable is set. if not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS", ""): session.skip("Credentials must be set via environment variable") + # Install pyopenssl for mTLS testing. + if os.environ.get("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") == "true": + session.install("pyopenssl") system_test_exists = os.path.exists(system_test_path) system_test_folder_exists = os.path.exists(system_test_folder_path) @@ -129,9 +145,21 @@ def system(session): # Run py.test against the system tests. if system_test_exists: - session.run("py.test", "--quiet", system_test_path, *session.posargs) + session.run( + "py.test", + "--quiet", + f"--junitxml=system_{session.python}_sponge_log.xml", + system_test_path, + *session.posargs, + ) if system_test_folder_exists: - session.run("py.test", "--quiet", system_test_folder_path, *session.posargs) + session.run( + "py.test", + "--quiet", + f"--junitxml=system_{session.python}_sponge_log.xml", + system_test_folder_path, + *session.posargs, + ) @nox.session(python=DEFAULT_PYTHON_VERSION) diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index bca0522e..97bf7da8 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -85,7 +85,7 @@ def get_pytest_env_vars() -> Dict[str, str]: # DO NOT EDIT - automatically generated. # All versions used to tested samples. -ALL_VERSIONS = ["2.7", "3.6", "3.7", "3.8"] +ALL_VERSIONS = ["2.7", "3.6", "3.7", "3.8", "3.9"] # Any default versions that should be ignored. IGNORED_VERSIONS = TEST_CONFIG['ignored_versions'] diff --git a/synth.metadata b/synth.metadata index 6117686d..ed91bb7e 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,7 +4,7 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/python-documentai.git", - "sha": "d6f183a696b211c6d29bc28e9bbd0a8537f65577" + "sha": "b78db75d0f3c68a38fbc5e540d264f11f5abfd3b" } }, { @@ -19,14 +19,14 @@ "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "41a4e56982620d3edcf110d76f4fcdfdec471ac8" + "sha": "0780323da96d5a53925fe0547757181fe76e8f1e" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "41a4e56982620d3edcf110d76f4fcdfdec471ac8" + "sha": "0780323da96d5a53925fe0547757181fe76e8f1e" } } ], @@ -58,6 +58,7 @@ ".github/ISSUE_TEMPLATE/feature_request.md", ".github/ISSUE_TEMPLATE/support_request.md", ".github/PULL_REQUEST_TEMPLATE.md", + ".github/header-checker-lint.yml", ".github/release-please.yml", ".github/snippet-bot.yml", ".gitignore", From 74fabb5e260ecc27e9cf005502d79590fa7f72e4 Mon Sep 17 00:00:00 2001 From: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Date: Thu, 25 Mar 2021 09:17:12 -0600 Subject: [PATCH 29/30] feat: add documentai v1 (#101) --- .kokoro/samples/python3.6/periodic-head.cfg | 11 + .kokoro/samples/python3.7/periodic-head.cfg | 11 + .kokoro/samples/python3.8/periodic-head.cfg | 11 + .kokoro/test-samples-against-head.sh | 28 + .kokoro/test-samples-impl.sh | 102 + .kokoro/test-samples.sh | 96 +- .pre-commit-config.yaml | 2 +- .../document_processor_service.rst | 6 + docs/documentai_v1/services.rst | 6 + docs/documentai_v1/types.rst | 7 + docs/index.rst | 2 + google/cloud/documentai/__init__.py | 48 +- google/cloud/documentai_v1/__init__.py | 63 + google/cloud/documentai_v1/py.typed | 2 + .../cloud/documentai_v1/services/__init__.py | 16 + .../document_processor_service/__init__.py | 24 + .../async_client.py | 476 +++++ .../document_processor_service/client.py | 631 ++++++ .../transports/__init__.py | 37 + .../transports/base.py | 191 ++ .../transports/grpc.py | 333 ++++ .../transports/grpc_asyncio.py | 341 ++++ google/cloud/documentai_v1/types/__init__.py | 66 + google/cloud/documentai_v1/types/document.py | 1109 +++++++++++ .../cloud/documentai_v1/types/document_io.py | 136 ++ .../types/document_processor_service.py | 309 +++ google/cloud/documentai_v1/types/geometry.py | 78 + .../async_client.py | 38 +- .../document_understanding_service/client.py | 4 +- .../transports/base.py | 20 +- .../transports/grpc.py | 103 +- .../transports/grpc_asyncio.py | 111 +- .../documentai_v1beta2/types/__init__.py | 60 +- google/cloud/documentai_v1beta3/__init__.py | 16 + .../async_client.py | 35 +- .../transports/base.py | 21 +- .../transports/grpc.py | 103 +- .../transports/grpc_asyncio.py | 111 +- .../documentai_v1beta3/types/__init__.py | 50 +- .../documentai_v1beta3/types/document.py | 91 +- .../documentai_v1beta3/types/document_io.py | 136 ++ .../types/document_processor_service.py | 125 ++ noxfile.py | 27 +- renovate.json | 3 +- scripts/fixup_documentai_v1beta2_keywords.py | 180 -- scripts/fixup_documentai_v1beta3_keywords.py | 181 -- scripts/fixup_keywords.py | 178 -- setup.py | 3 +- synth.metadata | 135 +- synth.py | 20 +- testing/constraints-3.10.txt | 0 testing/constraints-3.11.txt | 0 testing/constraints-3.6.txt | 9 + testing/constraints-3.7.txt | 0 testing/constraints-3.8.txt | 0 testing/constraints-3.9.txt | 0 tests/unit/gapic/documentai_v1/__init__.py | 16 + .../test_document_processor_service.py | 1723 +++++++++++++++++ .../unit/gapic/documentai_v1beta2/__init__.py | 15 + .../test_document_understanding_service.py | 45 +- .../unit/gapic/documentai_v1beta3/__init__.py | 15 + .../test_document_processor_service.py | 62 +- 62 files changed, 6593 insertions(+), 1185 deletions(-) create mode 100644 .kokoro/samples/python3.6/periodic-head.cfg create mode 100644 .kokoro/samples/python3.7/periodic-head.cfg create mode 100644 .kokoro/samples/python3.8/periodic-head.cfg create mode 100755 .kokoro/test-samples-against-head.sh create mode 100755 .kokoro/test-samples-impl.sh create mode 100644 docs/documentai_v1/document_processor_service.rst create mode 100644 docs/documentai_v1/services.rst create mode 100644 docs/documentai_v1/types.rst create mode 100644 google/cloud/documentai_v1/__init__.py create mode 100644 google/cloud/documentai_v1/py.typed create mode 100644 google/cloud/documentai_v1/services/__init__.py create mode 100644 google/cloud/documentai_v1/services/document_processor_service/__init__.py create mode 100644 google/cloud/documentai_v1/services/document_processor_service/async_client.py create mode 100644 google/cloud/documentai_v1/services/document_processor_service/client.py create mode 100644 google/cloud/documentai_v1/services/document_processor_service/transports/__init__.py create mode 100644 google/cloud/documentai_v1/services/document_processor_service/transports/base.py create mode 100644 google/cloud/documentai_v1/services/document_processor_service/transports/grpc.py create mode 100644 google/cloud/documentai_v1/services/document_processor_service/transports/grpc_asyncio.py create mode 100644 google/cloud/documentai_v1/types/__init__.py create mode 100644 google/cloud/documentai_v1/types/document.py create mode 100644 google/cloud/documentai_v1/types/document_io.py create mode 100644 google/cloud/documentai_v1/types/document_processor_service.py create mode 100644 google/cloud/documentai_v1/types/geometry.py create mode 100644 google/cloud/documentai_v1beta3/types/document_io.py delete mode 100644 scripts/fixup_documentai_v1beta2_keywords.py delete mode 100644 scripts/fixup_documentai_v1beta3_keywords.py delete mode 100644 scripts/fixup_keywords.py create mode 100644 testing/constraints-3.10.txt create mode 100644 testing/constraints-3.11.txt create mode 100644 testing/constraints-3.6.txt create mode 100644 testing/constraints-3.7.txt create mode 100644 testing/constraints-3.8.txt create mode 100644 testing/constraints-3.9.txt create mode 100644 tests/unit/gapic/documentai_v1/__init__.py create mode 100644 tests/unit/gapic/documentai_v1/test_document_processor_service.py diff --git a/.kokoro/samples/python3.6/periodic-head.cfg b/.kokoro/samples/python3.6/periodic-head.cfg new file mode 100644 index 00000000..f9cfcd33 --- /dev/null +++ b/.kokoro/samples/python3.6/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-pubsub/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/samples/python3.7/periodic-head.cfg b/.kokoro/samples/python3.7/periodic-head.cfg new file mode 100644 index 00000000..f9cfcd33 --- /dev/null +++ b/.kokoro/samples/python3.7/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-pubsub/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/samples/python3.8/periodic-head.cfg b/.kokoro/samples/python3.8/periodic-head.cfg new file mode 100644 index 00000000..f9cfcd33 --- /dev/null +++ b/.kokoro/samples/python3.8/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-pubsub/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/test-samples-against-head.sh b/.kokoro/test-samples-against-head.sh new file mode 100755 index 00000000..d04ee4fd --- /dev/null +++ b/.kokoro/test-samples-against-head.sh @@ -0,0 +1,28 @@ +#!/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 +# +# 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. + +# A customized test runner for samples. +# +# For periodic builds, you can specify this file for testing against head. + +# `-e` enables the script to automatically fail when a command fails +# `-o pipefail` sets the exit code to the rightmost comment to exit with a non-zero +set -eo pipefail +# Enables `**` to include files nested inside sub-folders +shopt -s globstar + +cd github/python-documentai + +exec .kokoro/test-samples-impl.sh diff --git a/.kokoro/test-samples-impl.sh b/.kokoro/test-samples-impl.sh new file mode 100755 index 00000000..cf5de74c --- /dev/null +++ b/.kokoro/test-samples-impl.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# Copyright 2021 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. + + +# `-e` enables the script to automatically fail when a command fails +# `-o pipefail` sets the exit code to the rightmost comment to exit with a non-zero +set -eo pipefail +# Enables `**` to include files nested inside sub-folders +shopt -s globstar + +# 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 + +# Debug: show build environment +env | grep KOKORO + +# Install nox +python3.6 -m pip install --upgrade --quiet nox + +# Use secrets acessor service account to get secrets +if [[ -f "${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" ]]; then + gcloud auth activate-service-account \ + --key-file="${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" \ + --project="cloud-devrel-kokoro-resources" +fi + +# This script will create 3 files: +# - testing/test-env.sh +# - testing/service-account.json +# - testing/client-secrets.json +./scripts/decrypt-secrets.sh + +source ./testing/test-env.sh +export GOOGLE_APPLICATION_CREDENTIALS=$(pwd)/testing/service-account.json + +# For cloud-run session, we activate the service account for gcloud sdk. +gcloud auth activate-service-account \ + --key-file "${GOOGLE_APPLICATION_CREDENTIALS}" + +export GOOGLE_CLIENT_SECRETS=$(pwd)/testing/client-secrets.json + +echo -e "\n******************** TESTING PROJECTS ********************" + +# Switch to 'fail at end' to allow all tests to complete before exiting. +set +e +# Use RTN to return a non-zero value if the test fails. +RTN=0 +ROOT=$(pwd) +# Find all requirements.txt in the samples directory (may break on whitespace). +for file in samples/**/requirements.txt; do + cd "$ROOT" + # Navigate to the project folder. + file=$(dirname "$file") + cd "$file" + + echo "------------------------------------------------------------" + echo "- testing $file" + echo "------------------------------------------------------------" + + # Use nox to execute the tests for the project. + python3.6 -m nox -s "$RUN_TESTS_SESSION" + EXIT=$? + + # If this is a periodic build, send the test log to the FlakyBot. + # See https://github.com/googleapis/repo-automation-bots/tree/master/packages/flakybot. + if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"periodic"* ]]; then + chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot + $KOKORO_GFILE_DIR/linux_amd64/flakybot + fi + + if [[ $EXIT -ne 0 ]]; then + RTN=1 + echo -e "\n Testing failed: Nox returned a non-zero exit code. \n" + else + echo -e "\n Testing completed.\n" + fi + +done +cd "$ROOT" + +# Workaround for Kokoro permissions issue: delete secrets +rm testing/{test-env.sh,client-secrets.json,service-account.json} + +exit "$RTN" diff --git a/.kokoro/test-samples.sh b/.kokoro/test-samples.sh index eca0482e..7dd0adac 100755 --- a/.kokoro/test-samples.sh +++ b/.kokoro/test-samples.sh @@ -13,6 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +# The default test runner for samples. +# +# For periodic builds, we rewinds the repo to the latest release, and +# run test-samples-impl.sh. # `-e` enables the script to automatically fail when a command fails # `-o pipefail` sets the exit code to the rightmost comment to exit with a non-zero @@ -24,87 +28,19 @@ cd github/python-documentai # Run periodic samples tests at latest release if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"periodic"* ]]; then + # preserving the test runner implementation. + cp .kokoro/test-samples-impl.sh "${TMPDIR}/test-samples-impl.sh" + echo "--- IMPORTANT IMPORTANT IMPORTANT ---" + echo "Now we rewind the repo back to the latest release..." LATEST_RELEASE=$(git describe --abbrev=0 --tags) 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 - -# Debug: show build environment -env | grep KOKORO - -# Install nox -python3.6 -m pip install --upgrade --quiet nox - -# Use secrets acessor service account to get secrets -if [[ -f "${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" ]]; then - gcloud auth activate-service-account \ - --key-file="${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" \ - --project="cloud-devrel-kokoro-resources" -fi - -# This script will create 3 files: -# - testing/test-env.sh -# - testing/service-account.json -# - testing/client-secrets.json -./scripts/decrypt-secrets.sh - -source ./testing/test-env.sh -export GOOGLE_APPLICATION_CREDENTIALS=$(pwd)/testing/service-account.json - -# For cloud-run session, we activate the service account for gcloud sdk. -gcloud auth activate-service-account \ - --key-file "${GOOGLE_APPLICATION_CREDENTIALS}" - -export GOOGLE_CLIENT_SECRETS=$(pwd)/testing/client-secrets.json - -echo -e "\n******************** TESTING PROJECTS ********************" - -# Switch to 'fail at end' to allow all tests to complete before exiting. -set +e -# Use RTN to return a non-zero value if the test fails. -RTN=0 -ROOT=$(pwd) -# Find all requirements.txt in the samples directory (may break on whitespace). -for file in samples/**/requirements.txt; do - cd "$ROOT" - # Navigate to the project folder. - file=$(dirname "$file") - cd "$file" - - echo "------------------------------------------------------------" - echo "- testing $file" - echo "------------------------------------------------------------" - - # Use nox to execute the tests for the project. - python3.6 -m nox -s "$RUN_TESTS_SESSION" - EXIT=$? - - # If this is a periodic build, send the test log to the FlakyBot. - # See https://github.com/googleapis/repo-automation-bots/tree/master/packages/flakybot. - if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"periodic"* ]]; then - chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot - $KOKORO_GFILE_DIR/linux_amd64/flakybot + echo "The current head is: " + echo $(git rev-parse --verify HEAD) + echo "--- IMPORTANT IMPORTANT IMPORTANT ---" + # move back the test runner implementation if there's no file. + if [ ! -f .kokoro/test-samples-impl.sh ]; then + cp "${TMPDIR}/test-samples-impl.sh" .kokoro/test-samples-impl.sh fi +fi - if [[ $EXIT -ne 0 ]]; then - RTN=1 - echo -e "\n Testing failed: Nox returned a non-zero exit code. \n" - else - echo -e "\n Testing completed.\n" - fi - -done -cd "$ROOT" - -# Workaround for Kokoro permissions issue: delete secrets -rm testing/{test-env.sh,client-secrets.json,service-account.json} - -exit "$RTN" +exec .kokoro/test-samples-impl.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a9024b15..32302e48 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,6 @@ repos: hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 + rev: 3.9.0 hooks: - id: flake8 diff --git a/docs/documentai_v1/document_processor_service.rst b/docs/documentai_v1/document_processor_service.rst new file mode 100644 index 00000000..3918355b --- /dev/null +++ b/docs/documentai_v1/document_processor_service.rst @@ -0,0 +1,6 @@ +DocumentProcessorService +------------------------------------------ + +.. automodule:: google.cloud.documentai_v1.services.document_processor_service + :members: + :inherited-members: diff --git a/docs/documentai_v1/services.rst b/docs/documentai_v1/services.rst new file mode 100644 index 00000000..551bb666 --- /dev/null +++ b/docs/documentai_v1/services.rst @@ -0,0 +1,6 @@ +Services for Google Cloud Documentai v1 API +=========================================== +.. toctree:: + :maxdepth: 2 + + document_processor_service diff --git a/docs/documentai_v1/types.rst b/docs/documentai_v1/types.rst new file mode 100644 index 00000000..68ac7119 --- /dev/null +++ b/docs/documentai_v1/types.rst @@ -0,0 +1,7 @@ +Types for Google Cloud Documentai v1 API +======================================== + +.. automodule:: google.cloud.documentai_v1.types + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst index c6c5efde..fd5e2754 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,6 +7,8 @@ API Reference .. toctree:: :maxdepth: 2 + documentai_v1/services + documentai_v1/types documentai_v1beta3/services documentai_v1beta3/types documentai_v1beta2/services diff --git a/google/cloud/documentai/__init__.py b/google/cloud/documentai/__init__.py index edd80431..b488ee65 100644 --- a/google/cloud/documentai/__init__.py +++ b/google/cloud/documentai/__init__.py @@ -15,52 +15,68 @@ # limitations under the License. # -from google.cloud.documentai_v1beta3.services.document_processor_service.async_client import ( +from google.cloud.documentai_v1.services.document_processor_service.async_client import ( DocumentProcessorServiceAsyncClient, ) -from google.cloud.documentai_v1beta3.services.document_processor_service.client import ( +from google.cloud.documentai_v1.services.document_processor_service.client import ( DocumentProcessorServiceClient, ) -from google.cloud.documentai_v1beta3.types.document import Document -from google.cloud.documentai_v1beta3.types.document_processor_service import ( +from google.cloud.documentai_v1.types.document import Document +from google.cloud.documentai_v1.types.document_io import BatchDocumentsInputConfig +from google.cloud.documentai_v1.types.document_io import DocumentOutputConfig +from google.cloud.documentai_v1.types.document_io import GcsDocument +from google.cloud.documentai_v1.types.document_io import GcsDocuments +from google.cloud.documentai_v1.types.document_io import GcsPrefix +from google.cloud.documentai_v1.types.document_io import RawDocument +from google.cloud.documentai_v1.types.document_processor_service import ( BatchProcessMetadata, ) -from google.cloud.documentai_v1beta3.types.document_processor_service import ( +from google.cloud.documentai_v1.types.document_processor_service import ( BatchProcessRequest, ) -from google.cloud.documentai_v1beta3.types.document_processor_service import ( +from google.cloud.documentai_v1.types.document_processor_service import ( BatchProcessResponse, ) -from google.cloud.documentai_v1beta3.types.document_processor_service import ( - ProcessRequest, +from google.cloud.documentai_v1.types.document_processor_service import ( + CommonOperationMetadata, ) -from google.cloud.documentai_v1beta3.types.document_processor_service import ( - ProcessResponse, +from google.cloud.documentai_v1.types.document_processor_service import ( + HumanReviewStatus, ) -from google.cloud.documentai_v1beta3.types.document_processor_service import ( +from google.cloud.documentai_v1.types.document_processor_service import ProcessRequest +from google.cloud.documentai_v1.types.document_processor_service import ProcessResponse +from google.cloud.documentai_v1.types.document_processor_service import ( ReviewDocumentOperationMetadata, ) -from google.cloud.documentai_v1beta3.types.document_processor_service import ( +from google.cloud.documentai_v1.types.document_processor_service import ( ReviewDocumentRequest, ) -from google.cloud.documentai_v1beta3.types.document_processor_service import ( +from google.cloud.documentai_v1.types.document_processor_service import ( ReviewDocumentResponse, ) -from google.cloud.documentai_v1beta3.types.geometry import BoundingPoly -from google.cloud.documentai_v1beta3.types.geometry import NormalizedVertex -from google.cloud.documentai_v1beta3.types.geometry import Vertex +from google.cloud.documentai_v1.types.geometry import BoundingPoly +from google.cloud.documentai_v1.types.geometry import NormalizedVertex +from google.cloud.documentai_v1.types.geometry import Vertex __all__ = ( + "BatchDocumentsInputConfig", "BatchProcessMetadata", "BatchProcessRequest", "BatchProcessResponse", "BoundingPoly", + "CommonOperationMetadata", "Document", + "DocumentOutputConfig", "DocumentProcessorServiceAsyncClient", "DocumentProcessorServiceClient", + "GcsDocument", + "GcsDocuments", + "GcsPrefix", + "HumanReviewStatus", "NormalizedVertex", "ProcessRequest", "ProcessResponse", + "RawDocument", "ReviewDocumentOperationMetadata", "ReviewDocumentRequest", "ReviewDocumentResponse", diff --git a/google/cloud/documentai_v1/__init__.py b/google/cloud/documentai_v1/__init__.py new file mode 100644 index 00000000..84d917be --- /dev/null +++ b/google/cloud/documentai_v1/__init__.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +# 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. +# + +from .services.document_processor_service import DocumentProcessorServiceClient +from .types.document import Document +from .types.document_io import BatchDocumentsInputConfig +from .types.document_io import DocumentOutputConfig +from .types.document_io import GcsDocument +from .types.document_io import GcsDocuments +from .types.document_io import GcsPrefix +from .types.document_io import RawDocument +from .types.document_processor_service import BatchProcessMetadata +from .types.document_processor_service import BatchProcessRequest +from .types.document_processor_service import BatchProcessResponse +from .types.document_processor_service import CommonOperationMetadata +from .types.document_processor_service import HumanReviewStatus +from .types.document_processor_service import ProcessRequest +from .types.document_processor_service import ProcessResponse +from .types.document_processor_service import ReviewDocumentOperationMetadata +from .types.document_processor_service import ReviewDocumentRequest +from .types.document_processor_service import ReviewDocumentResponse +from .types.geometry import BoundingPoly +from .types.geometry import NormalizedVertex +from .types.geometry import Vertex + + +__all__ = ( + "BatchDocumentsInputConfig", + "BatchProcessMetadata", + "BatchProcessRequest", + "BatchProcessResponse", + "BoundingPoly", + "CommonOperationMetadata", + "Document", + "DocumentOutputConfig", + "GcsDocument", + "GcsDocuments", + "GcsPrefix", + "HumanReviewStatus", + "NormalizedVertex", + "ProcessRequest", + "ProcessResponse", + "RawDocument", + "ReviewDocumentOperationMetadata", + "ReviewDocumentRequest", + "ReviewDocumentResponse", + "Vertex", + "DocumentProcessorServiceClient", +) diff --git a/google/cloud/documentai_v1/py.typed b/google/cloud/documentai_v1/py.typed new file mode 100644 index 00000000..81b45001 --- /dev/null +++ b/google/cloud/documentai_v1/py.typed @@ -0,0 +1,2 @@ +# Marker file for PEP 561. +# The google-cloud-documentai package uses inline types. diff --git a/google/cloud/documentai_v1/services/__init__.py b/google/cloud/documentai_v1/services/__init__.py new file mode 100644 index 00000000..42ffdf2b --- /dev/null +++ b/google/cloud/documentai_v1/services/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +# 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. +# diff --git a/google/cloud/documentai_v1/services/document_processor_service/__init__.py b/google/cloud/documentai_v1/services/document_processor_service/__init__.py new file mode 100644 index 00000000..9f87d9f4 --- /dev/null +++ b/google/cloud/documentai_v1/services/document_processor_service/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +# 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. +# + +from .client import DocumentProcessorServiceClient +from .async_client import DocumentProcessorServiceAsyncClient + +__all__ = ( + "DocumentProcessorServiceClient", + "DocumentProcessorServiceAsyncClient", +) diff --git a/google/cloud/documentai_v1/services/document_processor_service/async_client.py b/google/cloud/documentai_v1/services/document_processor_service/async_client.py new file mode 100644 index 00000000..42cf58a4 --- /dev/null +++ b/google/cloud/documentai_v1/services/document_processor_service/async_client.py @@ -0,0 +1,476 @@ +# -*- coding: utf-8 -*- + +# 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. +# + +from collections import OrderedDict +import functools +import re +from typing import Dict, Sequence, Tuple, Type, Union +import pkg_resources + +import google.api_core.client_options as ClientOptions # 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.oauth2 import service_account # type: ignore + +from google.api_core import operation # type: ignore +from google.api_core import operation_async # type: ignore +from google.cloud.documentai_v1.types import document +from google.cloud.documentai_v1.types import document_processor_service + +from .transports.base import DocumentProcessorServiceTransport, DEFAULT_CLIENT_INFO +from .transports.grpc_asyncio import DocumentProcessorServiceGrpcAsyncIOTransport +from .client import DocumentProcessorServiceClient + + +class DocumentProcessorServiceAsyncClient: + """Service to call Cloud DocumentAI to process documents + according to the processor's definition. Processors are built + using state-of-the-art Google AI such as natural language, + computer vision, and translation to extract structured + information from unstructured or semi-structured documents. + """ + + _client: DocumentProcessorServiceClient + + DEFAULT_ENDPOINT = DocumentProcessorServiceClient.DEFAULT_ENDPOINT + DEFAULT_MTLS_ENDPOINT = DocumentProcessorServiceClient.DEFAULT_MTLS_ENDPOINT + + human_review_config_path = staticmethod( + DocumentProcessorServiceClient.human_review_config_path + ) + parse_human_review_config_path = staticmethod( + DocumentProcessorServiceClient.parse_human_review_config_path + ) + processor_path = staticmethod(DocumentProcessorServiceClient.processor_path) + parse_processor_path = staticmethod( + DocumentProcessorServiceClient.parse_processor_path + ) + + common_billing_account_path = staticmethod( + DocumentProcessorServiceClient.common_billing_account_path + ) + parse_common_billing_account_path = staticmethod( + DocumentProcessorServiceClient.parse_common_billing_account_path + ) + + common_folder_path = staticmethod(DocumentProcessorServiceClient.common_folder_path) + parse_common_folder_path = staticmethod( + DocumentProcessorServiceClient.parse_common_folder_path + ) + + common_organization_path = staticmethod( + DocumentProcessorServiceClient.common_organization_path + ) + parse_common_organization_path = staticmethod( + DocumentProcessorServiceClient.parse_common_organization_path + ) + + common_project_path = staticmethod( + DocumentProcessorServiceClient.common_project_path + ) + parse_common_project_path = staticmethod( + DocumentProcessorServiceClient.parse_common_project_path + ) + + common_location_path = staticmethod( + DocumentProcessorServiceClient.common_location_path + ) + parse_common_location_path = staticmethod( + DocumentProcessorServiceClient.parse_common_location_path + ) + + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + DocumentProcessorServiceAsyncClient: The constructed client. + """ + return DocumentProcessorServiceClient.from_service_account_info.__func__(DocumentProcessorServiceAsyncClient, info, *args, **kwargs) # type: ignore + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + DocumentProcessorServiceAsyncClient: The constructed client. + """ + return DocumentProcessorServiceClient.from_service_account_file.__func__(DocumentProcessorServiceAsyncClient, filename, *args, **kwargs) # type: ignore + + from_service_account_json = from_service_account_file + + @property + def transport(self) -> DocumentProcessorServiceTransport: + """Return the transport used by the client instance. + + Returns: + DocumentProcessorServiceTransport: The transport used by the client instance. + """ + return self._client.transport + + get_transport_class = functools.partial( + type(DocumentProcessorServiceClient).get_transport_class, + type(DocumentProcessorServiceClient), + ) + + def __init__( + self, + *, + credentials: credentials.Credentials = None, + transport: Union[str, DocumentProcessorServiceTransport] = "grpc_asyncio", + client_options: ClientOptions = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiate the document processor service client. + + Args: + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + transport (Union[str, ~.DocumentProcessorServiceTransport]): 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. + (1) The ``api_endpoint`` property can be used to override the + 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) 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 + creation failed for any reason. + """ + + self._client = DocumentProcessorServiceClient( + credentials=credentials, + transport=transport, + client_options=client_options, + client_info=client_info, + ) + + async def process_document( + self, + request: document_processor_service.ProcessRequest = None, + *, + name: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> document_processor_service.ProcessResponse: + r"""Processes a single document. + + Args: + request (:class:`google.cloud.documentai_v1.types.ProcessRequest`): + The request object. Request message for the process + document method. + name (:class:`str`): + Required. The processor resource + name. + + This corresponds to the ``name`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + google.cloud.documentai_v1.types.ProcessResponse: + Response message for the process + document method. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([name]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + request = document_processor_service.ProcessRequest(request) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + + if name is not None: + request.name = name + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method_async.wrap_method( + self._client._transport.process_document, + default_retry=retries.Retry( + initial=0.1, + maximum=60.0, + multiplier=1.3, + predicate=retries.if_exception_type( + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + ), + deadline=120.0, + ), + default_timeout=120.0, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("name", request.name),)), + ) + + # Send the request. + response = await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + + async def batch_process_documents( + self, + request: document_processor_service.BatchProcessRequest = None, + *, + name: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operation_async.AsyncOperation: + r"""LRO endpoint to batch process many documents. The output is + written to Cloud Storage as JSON in the [Document] format. + + Args: + request (:class:`google.cloud.documentai_v1.types.BatchProcessRequest`): + The request object. Request message for batch process + document method. + name (:class:`str`): + Required. The processor resource + name. + + This corresponds to the ``name`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + google.api_core.operation_async.AsyncOperation: + An object representing a long-running operation. + + The result type for the operation will be + :class:`google.cloud.documentai_v1.types.BatchProcessResponse` + Response message for batch process document method. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([name]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + request = document_processor_service.BatchProcessRequest(request) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + + if name is not None: + request.name = name + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method_async.wrap_method( + self._client._transport.batch_process_documents, + default_retry=retries.Retry( + initial=0.1, + maximum=60.0, + multiplier=1.3, + predicate=retries.if_exception_type( + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + ), + deadline=120.0, + ), + default_timeout=120.0, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("name", request.name),)), + ) + + # Send the request. + response = await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Wrap the response in an operation future. + response = operation_async.from_gapic( + response, + self._client._transport.operations_client, + document_processor_service.BatchProcessResponse, + metadata_type=document_processor_service.BatchProcessMetadata, + ) + + # Done; return the response. + return response + + async def review_document( + self, + request: document_processor_service.ReviewDocumentRequest = None, + *, + human_review_config: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operation_async.AsyncOperation: + r"""Send a document for Human Review. The input document + should be processed by the specified processor. + + Args: + request (:class:`google.cloud.documentai_v1.types.ReviewDocumentRequest`): + The request object. Request message for review document + method. + human_review_config (:class:`str`): + Required. The resource name of the + HumanReviewConfig that the document will + be reviewed with. + + This corresponds to the ``human_review_config`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + google.api_core.operation_async.AsyncOperation: + An object representing a long-running operation. + + The result type for the operation will be + :class:`google.cloud.documentai_v1.types.ReviewDocumentResponse` + Response message for review document method. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([human_review_config]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + request = document_processor_service.ReviewDocumentRequest(request) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + + if human_review_config is not None: + request.human_review_config = human_review_config + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method_async.wrap_method( + self._client._transport.review_document, + default_retry=retries.Retry( + initial=0.1, + maximum=60.0, + multiplier=1.3, + predicate=retries.if_exception_type( + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + ), + deadline=120.0, + ), + default_timeout=120.0, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("human_review_config", request.human_review_config),) + ), + ) + + # Send the request. + response = await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Wrap the response in an operation future. + response = operation_async.from_gapic( + response, + self._client._transport.operations_client, + document_processor_service.ReviewDocumentResponse, + metadata_type=document_processor_service.ReviewDocumentOperationMetadata, + ) + + # Done; return the response. + return response + + +try: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=pkg_resources.get_distribution( + "google-cloud-documentai", + ).version, + ) +except pkg_resources.DistributionNotFound: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() + + +__all__ = ("DocumentProcessorServiceAsyncClient",) diff --git a/google/cloud/documentai_v1/services/document_processor_service/client.py b/google/cloud/documentai_v1/services/document_processor_service/client.py new file mode 100644 index 00000000..46160b76 --- /dev/null +++ b/google/cloud/documentai_v1/services/document_processor_service/client.py @@ -0,0 +1,631 @@ +# -*- coding: utf-8 -*- + +# 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. +# + +from collections import OrderedDict +from distutils import util +import os +import re +from typing import Callable, Dict, Optional, Sequence, Tuple, Type, Union +import pkg_resources + +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 # type: ignore +from google.api_core import operation_async # type: ignore +from google.cloud.documentai_v1.types import document +from google.cloud.documentai_v1.types import document_processor_service + +from .transports.base import DocumentProcessorServiceTransport, DEFAULT_CLIENT_INFO +from .transports.grpc import DocumentProcessorServiceGrpcTransport +from .transports.grpc_asyncio import DocumentProcessorServiceGrpcAsyncIOTransport + + +class DocumentProcessorServiceClientMeta(type): + """Metaclass for the DocumentProcessorService client. + + This provides class-level methods for building and retrieving + support objects (e.g. transport) without polluting the client instance + objects. + """ + + _transport_registry = ( + OrderedDict() + ) # type: Dict[str, Type[DocumentProcessorServiceTransport]] + _transport_registry["grpc"] = DocumentProcessorServiceGrpcTransport + _transport_registry["grpc_asyncio"] = DocumentProcessorServiceGrpcAsyncIOTransport + + def get_transport_class( + cls, label: str = None, + ) -> Type[DocumentProcessorServiceTransport]: + """Return an appropriate transport class. + + Args: + label: The name of the desired transport. If none is + provided, then the first transport in the registry is used. + + Returns: + The transport class to use. + """ + # If a specific transport is requested, return that one. + if label: + return cls._transport_registry[label] + + # No transport is requested; return the default (that is, the first one + # in the dictionary). + return next(iter(cls._transport_registry.values())) + + +class DocumentProcessorServiceClient(metaclass=DocumentProcessorServiceClientMeta): + """Service to call Cloud DocumentAI to process documents + according to the processor's definition. Processors are built + using state-of-the-art Google AI such as natural language, + computer vision, and translation to extract structured + information from unstructured or semi-structured documents. + """ + + @staticmethod + def _get_default_mtls_endpoint(api_endpoint): + """Convert api endpoint to mTLS endpoint. + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + str: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + DEFAULT_ENDPOINT = "us-documentai.googleapis.com" + DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_ENDPOINT + ) + + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + DocumentProcessorServiceClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_info(info) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + DocumentProcessorServiceClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_file(filename) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + + from_service_account_json = from_service_account_file + + @property + def transport(self) -> DocumentProcessorServiceTransport: + """Return the transport used by the client instance. + + Returns: + DocumentProcessorServiceTransport: The transport used by the client instance. + """ + return self._transport + + @staticmethod + def human_review_config_path(project: str, location: str, processor: str,) -> str: + """Return a fully-qualified human_review_config string.""" + return "projects/{project}/locations/{location}/processors/{processor}/humanReviewConfig".format( + project=project, location=location, processor=processor, + ) + + @staticmethod + def parse_human_review_config_path(path: str) -> Dict[str, str]: + """Parse a human_review_config path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/processors/(?P.+?)/humanReviewConfig$", + path, + ) + return m.groupdict() if m else {} + + @staticmethod + def processor_path(project: str, location: str, processor: str,) -> str: + """Return a fully-qualified processor string.""" + return "projects/{project}/locations/{location}/processors/{processor}".format( + project=project, location=location, processor=processor, + ) + + @staticmethod + def parse_processor_path(path: str) -> Dict[str, str]: + """Parse a processor path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/processors/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + + @staticmethod + def common_billing_account_path(billing_account: str,) -> str: + """Return a fully-qualified billing_account string.""" + return "billingAccounts/{billing_account}".format( + billing_account=billing_account, + ) + + @staticmethod + def parse_common_billing_account_path(path: str) -> Dict[str, str]: + """Parse a billing_account path into its component segments.""" + m = re.match(r"^billingAccounts/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_folder_path(folder: str,) -> str: + """Return a fully-qualified folder string.""" + return "folders/{folder}".format(folder=folder,) + + @staticmethod + def parse_common_folder_path(path: str) -> Dict[str, str]: + """Parse a folder path into its component segments.""" + m = re.match(r"^folders/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_organization_path(organization: str,) -> str: + """Return a fully-qualified organization string.""" + return "organizations/{organization}".format(organization=organization,) + + @staticmethod + def parse_common_organization_path(path: str) -> Dict[str, str]: + """Parse a organization path into its component segments.""" + m = re.match(r"^organizations/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_project_path(project: str,) -> str: + """Return a fully-qualified project string.""" + return "projects/{project}".format(project=project,) + + @staticmethod + def parse_common_project_path(path: str) -> Dict[str, str]: + """Parse a project path into its component segments.""" + m = re.match(r"^projects/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_location_path(project: str, location: str,) -> str: + """Return a fully-qualified location string.""" + return "projects/{project}/locations/{location}".format( + project=project, location=location, + ) + + @staticmethod + def parse_common_location_path(path: str) -> Dict[str, str]: + """Parse a location path into its component segments.""" + m = re.match(r"^projects/(?P.+?)/locations/(?P.+?)$", path) + return m.groupdict() if m else {} + + def __init__( + self, + *, + credentials: Optional[credentials.Credentials] = None, + transport: Union[str, DocumentProcessorServiceTransport, None] = None, + client_options: Optional[client_options_lib.ClientOptions] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiate the document processor service client. + + Args: + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + transport (Union[str, DocumentProcessorServiceTransport]): The + transport to use. If set to None, a transport is chosen + automatically. + client_options (google.api_core.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_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) 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 = client_options_lib.from_dict(client_options) + if client_options is None: + client_options = client_options_lib.ClientOptions() + + # Create SSL credentials for mutual TLS if needed. + use_client_cert = bool( + util.strtobool(os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false")) + ) + + client_cert_source_func = None + is_mtls = False + if use_client_cert: + if client_options.client_cert_source: + is_mtls = True + client_cert_source_func = client_options.client_cert_source + else: + is_mtls = mtls.has_default_client_cert_source() + client_cert_source_func = ( + mtls.default_client_cert_source() 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": + api_endpoint = self.DEFAULT_ENDPOINT + elif use_mtls_env == "always": + api_endpoint = self.DEFAULT_MTLS_ENDPOINT + elif use_mtls_env == "auto": + api_endpoint = ( + self.DEFAULT_MTLS_ENDPOINT if is_mtls else self.DEFAULT_ENDPOINT + ) + else: + raise MutualTLSChannelError( + "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted values: never, auto, always" + ) + + # Save or instantiate the transport. + # Ordinarily, we provide the transport, but allowing a custom transport + # instance provides an extensibility point for unusual situations. + if isinstance(transport, DocumentProcessorServiceTransport): + # transport is a DocumentProcessorServiceTransport instance. + if credentials or client_options.credentials_file: + raise ValueError( + "When providing a transport instance, " + "provide its credentials directly." + ) + if client_options.scopes: + raise ValueError( + "When providing a transport instance, " + "provide its scopes directly." + ) + self._transport = transport + else: + Transport = type(self).get_transport_class(transport) + self._transport = Transport( + credentials=credentials, + credentials_file=client_options.credentials_file, + host=api_endpoint, + scopes=client_options.scopes, + client_cert_source_for_mtls=client_cert_source_func, + quota_project_id=client_options.quota_project_id, + client_info=client_info, + ) + + def process_document( + self, + request: document_processor_service.ProcessRequest = None, + *, + name: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> document_processor_service.ProcessResponse: + r"""Processes a single document. + + Args: + request (google.cloud.documentai_v1.types.ProcessRequest): + The request object. Request message for the process + document method. + name (str): + Required. The processor resource + name. + + This corresponds to the ``name`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + google.cloud.documentai_v1.types.ProcessResponse: + Response message for the process + document method. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([name]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # Minor optimization to avoid making a copy if the user passes + # in a document_processor_service.ProcessRequest. + # There's no risk of modifying the input as we've already verified + # there are no flattened fields. + if not isinstance(request, document_processor_service.ProcessRequest): + request = document_processor_service.ProcessRequest(request) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + + if name is not None: + request.name = name + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.process_document] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("name", request.name),)), + ) + + # Send the request. + response = rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + + def batch_process_documents( + self, + request: document_processor_service.BatchProcessRequest = None, + *, + name: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operation.Operation: + r"""LRO endpoint to batch process many documents. The output is + written to Cloud Storage as JSON in the [Document] format. + + Args: + request (google.cloud.documentai_v1.types.BatchProcessRequest): + The request object. Request message for batch process + document method. + name (str): + Required. The processor resource + name. + + This corresponds to the ``name`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + google.api_core.operation.Operation: + An object representing a long-running operation. + + The result type for the operation will be + :class:`google.cloud.documentai_v1.types.BatchProcessResponse` + Response message for batch process document method. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([name]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # Minor optimization to avoid making a copy if the user passes + # in a document_processor_service.BatchProcessRequest. + # There's no risk of modifying the input as we've already verified + # there are no flattened fields. + if not isinstance(request, document_processor_service.BatchProcessRequest): + request = document_processor_service.BatchProcessRequest(request) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + + if name is not None: + request.name = name + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.batch_process_documents] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("name", request.name),)), + ) + + # Send the request. + response = rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Wrap the response in an operation future. + response = operation.from_gapic( + response, + self._transport.operations_client, + document_processor_service.BatchProcessResponse, + metadata_type=document_processor_service.BatchProcessMetadata, + ) + + # Done; return the response. + return response + + def review_document( + self, + request: document_processor_service.ReviewDocumentRequest = None, + *, + human_review_config: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operation.Operation: + r"""Send a document for Human Review. The input document + should be processed by the specified processor. + + Args: + request (google.cloud.documentai_v1.types.ReviewDocumentRequest): + The request object. Request message for review document + method. + human_review_config (str): + Required. The resource name of the + HumanReviewConfig that the document will + be reviewed with. + + This corresponds to the ``human_review_config`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + google.api_core.operation.Operation: + An object representing a long-running operation. + + The result type for the operation will be + :class:`google.cloud.documentai_v1.types.ReviewDocumentResponse` + Response message for review document method. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([human_review_config]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # Minor optimization to avoid making a copy if the user passes + # in a document_processor_service.ReviewDocumentRequest. + # There's no risk of modifying the input as we've already verified + # there are no flattened fields. + if not isinstance(request, document_processor_service.ReviewDocumentRequest): + request = document_processor_service.ReviewDocumentRequest(request) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + + if human_review_config is not None: + request.human_review_config = human_review_config + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.review_document] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("human_review_config", request.human_review_config),) + ), + ) + + # Send the request. + response = rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Wrap the response in an operation future. + response = operation.from_gapic( + response, + self._transport.operations_client, + document_processor_service.ReviewDocumentResponse, + metadata_type=document_processor_service.ReviewDocumentOperationMetadata, + ) + + # Done; return the response. + return response + + +try: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=pkg_resources.get_distribution( + "google-cloud-documentai", + ).version, + ) +except pkg_resources.DistributionNotFound: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() + + +__all__ = ("DocumentProcessorServiceClient",) diff --git a/google/cloud/documentai_v1/services/document_processor_service/transports/__init__.py b/google/cloud/documentai_v1/services/document_processor_service/transports/__init__.py new file mode 100644 index 00000000..e3e820b3 --- /dev/null +++ b/google/cloud/documentai_v1/services/document_processor_service/transports/__init__.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +# 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. +# + +from collections import OrderedDict +from typing import Dict, Type + +from .base import DocumentProcessorServiceTransport +from .grpc import DocumentProcessorServiceGrpcTransport +from .grpc_asyncio import DocumentProcessorServiceGrpcAsyncIOTransport + + +# Compile a registry of transports. +_transport_registry = ( + OrderedDict() +) # type: Dict[str, Type[DocumentProcessorServiceTransport]] +_transport_registry["grpc"] = DocumentProcessorServiceGrpcTransport +_transport_registry["grpc_asyncio"] = DocumentProcessorServiceGrpcAsyncIOTransport + +__all__ = ( + "DocumentProcessorServiceTransport", + "DocumentProcessorServiceGrpcTransport", + "DocumentProcessorServiceGrpcAsyncIOTransport", +) diff --git a/google/cloud/documentai_v1/services/document_processor_service/transports/base.py b/google/cloud/documentai_v1/services/document_processor_service/transports/base.py new file mode 100644 index 00000000..cb344159 --- /dev/null +++ b/google/cloud/documentai_v1/services/document_processor_service/transports/base.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- + +# 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 abc +import typing +import pkg_resources + +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 +from google.api_core import operations_v1 # type: ignore +from google.auth import credentials # type: ignore + +from google.cloud.documentai_v1.types import document_processor_service +from google.longrunning import operations_pb2 as operations # type: ignore + + +try: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=pkg_resources.get_distribution( + "google-cloud-documentai", + ).version, + ) +except pkg_resources.DistributionNotFound: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() + + +class DocumentProcessorServiceTransport(abc.ABC): + """Abstract transport class for DocumentProcessorService.""" + + AUTH_SCOPES = ("https://www.googleapis.com/auth/cloud-platform",) + + def __init__( + self, + *, + host: str = "us-documentai.googleapis.com", + credentials: credentials.Credentials = None, + 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. + + Args: + host (Optional[str]): The hostname to connect to. + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + credentials_file (Optional[str]): A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is mutually exclusive with credentials. + 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: + host += ":443" + self._host = host + + # Save the scopes. + self._scopes = scopes or self.AUTH_SCOPES + + # If no credentials are provided, then determine the appropriate + # defaults. + if credentials and credentials_file: + raise exceptions.DuplicateCredentialArgs( + "'credentials_file' and 'credentials' are mutually exclusive" + ) + + if credentials_file is not None: + credentials, _ = auth.load_credentials_from_file( + credentials_file, scopes=self._scopes, quota_project_id=quota_project_id + ) + + elif credentials is None: + credentials, _ = auth.default( + scopes=self._scopes, quota_project_id=quota_project_id + ) + + # Save the credentials. + self._credentials = credentials + + def _prep_wrapped_messages(self, client_info): + # Precompute the wrapped methods. + self._wrapped_methods = { + self.process_document: gapic_v1.method.wrap_method( + self.process_document, + default_retry=retries.Retry( + initial=0.1, + maximum=60.0, + multiplier=1.3, + predicate=retries.if_exception_type( + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + ), + deadline=120.0, + ), + default_timeout=120.0, + client_info=client_info, + ), + self.batch_process_documents: gapic_v1.method.wrap_method( + self.batch_process_documents, + default_retry=retries.Retry( + initial=0.1, + maximum=60.0, + multiplier=1.3, + predicate=retries.if_exception_type( + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + ), + deadline=120.0, + ), + default_timeout=120.0, + client_info=client_info, + ), + self.review_document: gapic_v1.method.wrap_method( + self.review_document, + default_retry=retries.Retry( + initial=0.1, + maximum=60.0, + multiplier=1.3, + predicate=retries.if_exception_type( + exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, + ), + deadline=120.0, + ), + default_timeout=120.0, + client_info=client_info, + ), + } + + @property + def operations_client(self) -> operations_v1.OperationsClient: + """Return the client designed to process long-running operations.""" + raise NotImplementedError() + + @property + def process_document( + self, + ) -> typing.Callable[ + [document_processor_service.ProcessRequest], + typing.Union[ + document_processor_service.ProcessResponse, + typing.Awaitable[document_processor_service.ProcessResponse], + ], + ]: + raise NotImplementedError() + + @property + def batch_process_documents( + self, + ) -> typing.Callable[ + [document_processor_service.BatchProcessRequest], + typing.Union[operations.Operation, typing.Awaitable[operations.Operation]], + ]: + raise NotImplementedError() + + @property + def review_document( + self, + ) -> typing.Callable[ + [document_processor_service.ReviewDocumentRequest], + typing.Union[operations.Operation, typing.Awaitable[operations.Operation]], + ]: + raise NotImplementedError() + + +__all__ = ("DocumentProcessorServiceTransport",) diff --git a/google/cloud/documentai_v1/services/document_processor_service/transports/grpc.py b/google/cloud/documentai_v1/services/document_processor_service/transports/grpc.py new file mode 100644 index 00000000..8c55d69c --- /dev/null +++ b/google/cloud/documentai_v1/services/document_processor_service/transports/grpc.py @@ -0,0 +1,333 @@ +# -*- coding: utf-8 -*- + +# 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 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.documentai_v1.types import document_processor_service +from google.longrunning import operations_pb2 as operations # type: ignore + +from .base import DocumentProcessorServiceTransport, DEFAULT_CLIENT_INFO + + +class DocumentProcessorServiceGrpcTransport(DocumentProcessorServiceTransport): + """gRPC backend transport for DocumentProcessorService. + + Service to call Cloud DocumentAI to process documents + according to the processor's definition. Processors are built + using state-of-the-art Google AI such as natural language, + computer vision, and translation to extract structured + information from unstructured or semi-structured documents. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends protocol buffers over the wire using gRPC (which is built on + top of HTTP/2); the ``grpcio`` package must be installed. + """ + + _stubs: Dict[str, Callable] + + def __init__( + self, + *, + host: str = "us-documentai.googleapis.com", + credentials: credentials.Credentials = None, + credentials_file: str = None, + scopes: Sequence[str] = None, + channel: grpc.Channel = None, + api_mtls_endpoint: str = None, + client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, + ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: Callable[[], Tuple[bytes, bytes]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): The hostname to connect to. + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + This argument is ignored if ``channel`` is provided. + credentials_file (Optional[str]): A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + scopes (Optional(Sequence[str])): A list of scopes. This argument is + ignored if ``channel`` is provided. + channel (Optional[grpc.Channel]): A ``Channel`` instance through + which to make calls. + 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]]]): + 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. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` 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 + creation failed for any reason. + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + self._grpc_channel = None + self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + self._operations_client = None + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) + + if channel: + # Ignore credentials if a channel was passed. + credentials = False + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + self._ssl_channel_credentials = None + + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials + + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + ) + + if not self._grpc_channel: + self._grpc_channel = type(self).create_channel( + self._host, + credentials=self._credentials, + credentials_file=credentials_file, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, + quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) + + @classmethod + def create_channel( + cls, + host: str = "us-documentai.googleapis.com", + credentials: credentials.Credentials = None, + credentials_file: str = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + **kwargs, + ) -> grpc.Channel: + """Create and return a gRPC channel object. + Args: + host (Optional[str]): The host for the channel to use. + credentials (Optional[~.Credentials]): The + authorization credentials to attach to requests. These + credentials identify this application to the service. If + none are specified, the client will attempt to ascertain + the credentials from the environment. + credentials_file (Optional[str]): A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is mutually exclusive with credentials. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + kwargs (Optional[dict]): Keyword arguments, which are passed to the + channel creation. + Returns: + grpc.Channel: A gRPC channel object. + + Raises: + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + scopes = scopes or cls.AUTH_SCOPES + return grpc_helpers.create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + **kwargs, + ) + + @property + def grpc_channel(self) -> grpc.Channel: + """Return the channel designed to connect to this service. + """ + return self._grpc_channel + + @property + def operations_client(self) -> operations_v1.OperationsClient: + """Create the client designed to process long-running operations. + + This property caches on the instance; repeated calls return the same + client. + """ + # Sanity check: Only create a new client if we do not already have one. + if self._operations_client is None: + self._operations_client = operations_v1.OperationsClient(self.grpc_channel) + + # Return the client from cache. + return self._operations_client + + @property + def process_document( + self, + ) -> Callable[ + [document_processor_service.ProcessRequest], + document_processor_service.ProcessResponse, + ]: + r"""Return a callable for the process document method over gRPC. + + Processes a single document. + + Returns: + Callable[[~.ProcessRequest], + ~.ProcessResponse]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "process_document" not in self._stubs: + self._stubs["process_document"] = self.grpc_channel.unary_unary( + "/google.cloud.documentai.v1.DocumentProcessorService/ProcessDocument", + request_serializer=document_processor_service.ProcessRequest.serialize, + response_deserializer=document_processor_service.ProcessResponse.deserialize, + ) + return self._stubs["process_document"] + + @property + def batch_process_documents( + self, + ) -> Callable[ + [document_processor_service.BatchProcessRequest], operations.Operation + ]: + r"""Return a callable for the batch process documents method over gRPC. + + LRO endpoint to batch process many documents. The output is + written to Cloud Storage as JSON in the [Document] format. + + Returns: + Callable[[~.BatchProcessRequest], + ~.Operation]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "batch_process_documents" not in self._stubs: + self._stubs["batch_process_documents"] = self.grpc_channel.unary_unary( + "/google.cloud.documentai.v1.DocumentProcessorService/BatchProcessDocuments", + request_serializer=document_processor_service.BatchProcessRequest.serialize, + response_deserializer=operations.Operation.FromString, + ) + return self._stubs["batch_process_documents"] + + @property + def review_document( + self, + ) -> Callable[ + [document_processor_service.ReviewDocumentRequest], operations.Operation + ]: + r"""Return a callable for the review document method over gRPC. + + Send a document for Human Review. The input document + should be processed by the specified processor. + + Returns: + Callable[[~.ReviewDocumentRequest], + ~.Operation]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "review_document" not in self._stubs: + self._stubs["review_document"] = self.grpc_channel.unary_unary( + "/google.cloud.documentai.v1.DocumentProcessorService/ReviewDocument", + request_serializer=document_processor_service.ReviewDocumentRequest.serialize, + response_deserializer=operations.Operation.FromString, + ) + return self._stubs["review_document"] + + +__all__ = ("DocumentProcessorServiceGrpcTransport",) diff --git a/google/cloud/documentai_v1/services/document_processor_service/transports/grpc_asyncio.py b/google/cloud/documentai_v1/services/document_processor_service/transports/grpc_asyncio.py new file mode 100644 index 00000000..3b172f81 --- /dev/null +++ b/google/cloud/documentai_v1/services/document_processor_service/transports/grpc_asyncio.py @@ -0,0 +1,341 @@ +# -*- coding: utf-8 -*- + +# 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 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 + +import grpc # type: ignore +from grpc.experimental import aio # type: ignore + +from google.cloud.documentai_v1.types import document_processor_service +from google.longrunning import operations_pb2 as operations # type: ignore + +from .base import DocumentProcessorServiceTransport, DEFAULT_CLIENT_INFO +from .grpc import DocumentProcessorServiceGrpcTransport + + +class DocumentProcessorServiceGrpcAsyncIOTransport(DocumentProcessorServiceTransport): + """gRPC AsyncIO backend transport for DocumentProcessorService. + + Service to call Cloud DocumentAI to process documents + according to the processor's definition. Processors are built + using state-of-the-art Google AI such as natural language, + computer vision, and translation to extract structured + information from unstructured or semi-structured documents. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends protocol buffers over the wire using gRPC (which is built on + top of HTTP/2); the ``grpcio`` package must be installed. + """ + + _grpc_channel: aio.Channel + _stubs: Dict[str, Callable] = {} + + @classmethod + def create_channel( + cls, + host: str = "us-documentai.googleapis.com", + credentials: credentials.Credentials = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + **kwargs, + ) -> aio.Channel: + """Create and return a gRPC AsyncIO channel object. + Args: + host (Optional[str]): The host for the channel to use. + credentials (Optional[~.Credentials]): The + authorization credentials to attach to requests. These + credentials identify this application to the service. If + none are specified, the client will attempt to ascertain + the credentials from the environment. + credentials_file (Optional[str]): A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + kwargs (Optional[dict]): Keyword arguments, which are passed to the + channel creation. + Returns: + aio.Channel: A gRPC AsyncIO channel object. + """ + scopes = scopes or cls.AUTH_SCOPES + return grpc_helpers_async.create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + **kwargs, + ) + + def __init__( + self, + *, + host: str = "us-documentai.googleapis.com", + credentials: credentials.Credentials = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + channel: aio.Channel = None, + api_mtls_endpoint: str = None, + client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, + ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: Callable[[], Tuple[bytes, bytes]] = None, + quota_project_id=None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): The hostname to connect to. + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + This argument is ignored if ``channel`` is provided. + credentials_file (Optional[str]): A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + channel (Optional[aio.Channel]): A ``Channel`` instance through + which to make calls. + 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]]]): + 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. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` 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 + creation failed for any reason. + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + self._grpc_channel = None + self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + self._operations_client = None + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) + + if channel: + # Ignore credentials if a channel was passed. + credentials = False + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + self._ssl_channel_credentials = None + + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials + + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + ) + + if not self._grpc_channel: + self._grpc_channel = type(self).create_channel( + self._host, + credentials=self._credentials, + credentials_file=credentials_file, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, + quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) + + @property + def grpc_channel(self) -> aio.Channel: + """Create the channel designed to connect to this service. + + This property caches on the instance; repeated calls return + the same channel. + """ + # Return the channel from cache. + return self._grpc_channel + + @property + def operations_client(self) -> operations_v1.OperationsAsyncClient: + """Create the client designed to process long-running operations. + + This property caches on the instance; repeated calls return the same + client. + """ + # Sanity check: Only create a new client if we do not already have one. + if self._operations_client is None: + self._operations_client = operations_v1.OperationsAsyncClient( + self.grpc_channel + ) + + # Return the client from cache. + return self._operations_client + + @property + def process_document( + self, + ) -> Callable[ + [document_processor_service.ProcessRequest], + Awaitable[document_processor_service.ProcessResponse], + ]: + r"""Return a callable for the process document method over gRPC. + + Processes a single document. + + Returns: + Callable[[~.ProcessRequest], + Awaitable[~.ProcessResponse]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "process_document" not in self._stubs: + self._stubs["process_document"] = self.grpc_channel.unary_unary( + "/google.cloud.documentai.v1.DocumentProcessorService/ProcessDocument", + request_serializer=document_processor_service.ProcessRequest.serialize, + response_deserializer=document_processor_service.ProcessResponse.deserialize, + ) + return self._stubs["process_document"] + + @property + def batch_process_documents( + self, + ) -> Callable[ + [document_processor_service.BatchProcessRequest], + Awaitable[operations.Operation], + ]: + r"""Return a callable for the batch process documents method over gRPC. + + LRO endpoint to batch process many documents. The output is + written to Cloud Storage as JSON in the [Document] format. + + Returns: + Callable[[~.BatchProcessRequest], + Awaitable[~.Operation]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "batch_process_documents" not in self._stubs: + self._stubs["batch_process_documents"] = self.grpc_channel.unary_unary( + "/google.cloud.documentai.v1.DocumentProcessorService/BatchProcessDocuments", + request_serializer=document_processor_service.BatchProcessRequest.serialize, + response_deserializer=operations.Operation.FromString, + ) + return self._stubs["batch_process_documents"] + + @property + def review_document( + self, + ) -> Callable[ + [document_processor_service.ReviewDocumentRequest], + Awaitable[operations.Operation], + ]: + r"""Return a callable for the review document method over gRPC. + + Send a document for Human Review. The input document + should be processed by the specified processor. + + Returns: + Callable[[~.ReviewDocumentRequest], + Awaitable[~.Operation]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "review_document" not in self._stubs: + self._stubs["review_document"] = self.grpc_channel.unary_unary( + "/google.cloud.documentai.v1.DocumentProcessorService/ReviewDocument", + request_serializer=document_processor_service.ReviewDocumentRequest.serialize, + response_deserializer=operations.Operation.FromString, + ) + return self._stubs["review_document"] + + +__all__ = ("DocumentProcessorServiceGrpcAsyncIOTransport",) diff --git a/google/cloud/documentai_v1/types/__init__.py b/google/cloud/documentai_v1/types/__init__.py new file mode 100644 index 00000000..0d60bd37 --- /dev/null +++ b/google/cloud/documentai_v1/types/__init__.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +# 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. +# + +from .document import Document +from .document_io import ( + BatchDocumentsInputConfig, + DocumentOutputConfig, + GcsDocument, + GcsDocuments, + GcsPrefix, + RawDocument, +) +from .document_processor_service import ( + BatchProcessMetadata, + BatchProcessRequest, + BatchProcessResponse, + CommonOperationMetadata, + HumanReviewStatus, + ProcessRequest, + ProcessResponse, + ReviewDocumentOperationMetadata, + ReviewDocumentRequest, + ReviewDocumentResponse, +) +from .geometry import ( + BoundingPoly, + NormalizedVertex, + Vertex, +) + +__all__ = ( + "Document", + "BatchDocumentsInputConfig", + "DocumentOutputConfig", + "GcsDocument", + "GcsDocuments", + "GcsPrefix", + "RawDocument", + "BatchProcessMetadata", + "BatchProcessRequest", + "BatchProcessResponse", + "CommonOperationMetadata", + "HumanReviewStatus", + "ProcessRequest", + "ProcessResponse", + "ReviewDocumentOperationMetadata", + "ReviewDocumentRequest", + "ReviewDocumentResponse", + "BoundingPoly", + "NormalizedVertex", + "Vertex", +) diff --git a/google/cloud/documentai_v1/types/document.py b/google/cloud/documentai_v1/types/document.py new file mode 100644 index 00000000..781e3c55 --- /dev/null +++ b/google/cloud/documentai_v1/types/document.py @@ -0,0 +1,1109 @@ +# -*- coding: utf-8 -*- + +# 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 proto # type: ignore + + +from google.cloud.documentai_v1.types import geometry +from google.protobuf import timestamp_pb2 as timestamp # type: ignore +from google.rpc import status_pb2 as status # type: ignore +from google.type import color_pb2 as gt_color # type: ignore +from google.type import date_pb2 as date # type: ignore +from google.type import datetime_pb2 as datetime # type: ignore +from google.type import money_pb2 as money # type: ignore +from google.type import postal_address_pb2 as postal_address # type: ignore + + +__protobuf__ = proto.module( + package="google.cloud.documentai.v1", manifest={"Document",}, +) + + +class Document(proto.Message): + r"""Document represents the canonical document resource in + Document Understanding AI. + It is an interchange format that provides insights into + documents and allows for collaboration between users and + Document Understanding AI to iterate and optimize for quality. + + Attributes: + uri (str): + Optional. Currently supports Google Cloud Storage URI of the + form ``gs://bucket_name/object_name``. Object versioning is + not supported. See `Google Cloud Storage Request + URIs `__ + for more info. + content (bytes): + Optional. Inline document content, represented as a stream + of bytes. Note: As with all ``bytes`` fields, protobuffers + use a pure binary representation, whereas JSON + representations use base64. + mime_type (str): + An IANA published MIME type (also referred to + as media type). For more information, see + https://www.iana.org/assignments/media- + types/media-types.xhtml. + text (str): + Optional. UTF-8 encoded text in reading order + from the document. + text_styles (Sequence[google.cloud.documentai_v1.types.Document.Style]): + Styles for the + [Document.text][google.cloud.documentai.v1.Document.text]. + pages (Sequence[google.cloud.documentai_v1.types.Document.Page]): + Visual page layout for the + [Document][google.cloud.documentai.v1.Document]. + entities (Sequence[google.cloud.documentai_v1.types.Document.Entity]): + A list of entities detected on + [Document.text][google.cloud.documentai.v1.Document.text]. + For document shards, entities in this list may cross shard + boundaries. + entity_relations (Sequence[google.cloud.documentai_v1.types.Document.EntityRelation]): + Relationship among + [Document.entities][google.cloud.documentai.v1.Document.entities]. + text_changes (Sequence[google.cloud.documentai_v1.types.Document.TextChange]): + A list of text corrections made to [Document.text]. This is + usually used for annotating corrections to OCR mistakes. + Text changes for a given revision may not overlap with each + other. + shard_info (google.cloud.documentai_v1.types.Document.ShardInfo): + Information about the sharding if this + document is sharded part of a larger document. + If the document is not sharded, this message is + not specified. + error (google.rpc.status_pb2.Status): + Any error that occurred while processing this + document. + revisions (Sequence[google.cloud.documentai_v1.types.Document.Revision]): + Revision history of this document. + """ + + class ShardInfo(proto.Message): + r"""For a large document, sharding may be performed to produce + several document shards. Each document shard contains this field + to detail which shard it is. + + Attributes: + shard_index (int): + The 0-based index of this shard. + shard_count (int): + Total number of shards. + text_offset (int): + The index of the first character in + [Document.text][google.cloud.documentai.v1.Document.text] in + the overall document global text. + """ + + shard_index = proto.Field(proto.INT64, number=1) + + shard_count = proto.Field(proto.INT64, number=2) + + text_offset = proto.Field(proto.INT64, number=3) + + class Style(proto.Message): + r"""Annotation for common text style attributes. This adheres to + CSS conventions as much as possible. + + Attributes: + text_anchor (google.cloud.documentai_v1.types.Document.TextAnchor): + Text anchor indexing into the + [Document.text][google.cloud.documentai.v1.Document.text]. + color (google.type.color_pb2.Color): + Text color. + background_color (google.type.color_pb2.Color): + Text background color. + font_weight (str): + Font weight. Possible values are normal, bold, bolder, and + lighter. https://www.w3schools.com/cssref/pr_font_weight.asp + text_style (str): + Text style. Possible values are normal, italic, and oblique. + https://www.w3schools.com/cssref/pr_font_font-style.asp + text_decoration (str): + Text decoration. Follows CSS standard. + https://www.w3schools.com/cssref/pr_text_text-decoration.asp + font_size (google.cloud.documentai_v1.types.Document.Style.FontSize): + Font size. + """ + + class FontSize(proto.Message): + r"""Font size with unit. + + Attributes: + size (float): + Font size for the text. + unit (str): + Unit for the font size. Follows CSS naming + (in, px, pt, etc.). + """ + + size = proto.Field(proto.FLOAT, number=1) + + unit = proto.Field(proto.STRING, number=2) + + text_anchor = proto.Field( + proto.MESSAGE, number=1, message="Document.TextAnchor", + ) + + color = proto.Field(proto.MESSAGE, number=2, message=gt_color.Color,) + + background_color = proto.Field(proto.MESSAGE, number=3, message=gt_color.Color,) + + font_weight = proto.Field(proto.STRING, number=4) + + text_style = proto.Field(proto.STRING, number=5) + + text_decoration = proto.Field(proto.STRING, number=6) + + font_size = proto.Field( + proto.MESSAGE, number=7, message="Document.Style.FontSize", + ) + + class Page(proto.Message): + r"""A page in a [Document][google.cloud.documentai.v1.Document]. + + Attributes: + page_number (int): + 1-based index for current + [Page][google.cloud.documentai.v1.Document.Page] in a parent + [Document][google.cloud.documentai.v1.Document]. Useful when + a page is taken out of a + [Document][google.cloud.documentai.v1.Document] for + individual processing. + image (google.cloud.documentai_v1.types.Document.Page.Image): + Rendered image for this page. This image is + preprocessed to remove any skew, rotation, and + distortions such that the annotation bounding + boxes can be upright and axis-aligned. + transforms (Sequence[google.cloud.documentai_v1.types.Document.Page.Matrix]): + Transformation matrices that were applied to the original + document image to produce + [Page.image][google.cloud.documentai.v1.Document.Page.image]. + dimension (google.cloud.documentai_v1.types.Document.Page.Dimension): + Physical dimension of the page. + layout (google.cloud.documentai_v1.types.Document.Page.Layout): + [Layout][google.cloud.documentai.v1.Document.Page.Layout] + for the page. + detected_languages (Sequence[google.cloud.documentai_v1.types.Document.Page.DetectedLanguage]): + A list of detected languages together with + confidence. + blocks (Sequence[google.cloud.documentai_v1.types.Document.Page.Block]): + A list of visually detected text blocks on + the page. A block has a set of lines (collected + into paragraphs) that have a common line-spacing + and orientation. + paragraphs (Sequence[google.cloud.documentai_v1.types.Document.Page.Paragraph]): + A list of visually detected text paragraphs + on the page. A collection of lines that a human + would perceive as a paragraph. + lines (Sequence[google.cloud.documentai_v1.types.Document.Page.Line]): + A list of visually detected text lines on the + page. A collection of tokens that a human would + perceive as a line. + tokens (Sequence[google.cloud.documentai_v1.types.Document.Page.Token]): + A list of visually detected tokens on the + page. + visual_elements (Sequence[google.cloud.documentai_v1.types.Document.Page.VisualElement]): + A list of detected non-text visual elements + e.g. checkbox, signature etc. on the page. + tables (Sequence[google.cloud.documentai_v1.types.Document.Page.Table]): + A list of visually detected tables on the + page. + form_fields (Sequence[google.cloud.documentai_v1.types.Document.Page.FormField]): + A list of visually detected form fields on + the page. + """ + + class Dimension(proto.Message): + r"""Dimension for the page. + + Attributes: + width (float): + Page width. + height (float): + Page height. + unit (str): + Dimension unit. + """ + + width = proto.Field(proto.FLOAT, number=1) + + height = proto.Field(proto.FLOAT, number=2) + + unit = proto.Field(proto.STRING, number=3) + + class Image(proto.Message): + r"""Rendered image contents for this page. + + Attributes: + content (bytes): + Raw byte content of the image. + mime_type (str): + Encoding mime type for the image. + width (int): + Width of the image in pixels. + height (int): + Height of the image in pixels. + """ + + content = proto.Field(proto.BYTES, number=1) + + mime_type = proto.Field(proto.STRING, number=2) + + width = proto.Field(proto.INT32, number=3) + + height = proto.Field(proto.INT32, number=4) + + class Matrix(proto.Message): + r"""Representation for transformation matrix, intended to be + compatible and used with OpenCV format for image manipulation. + + Attributes: + rows (int): + Number of rows in the matrix. + cols (int): + Number of columns in the matrix. + type_ (int): + This encodes information about what data type the matrix + uses. For example, 0 (CV_8U) is an unsigned 8-bit image. For + the full list of OpenCV primitive data types, please refer + to + https://docs.opencv.org/4.3.0/d1/d1b/group__core__hal__interface.html + data (bytes): + The matrix data. + """ + + rows = proto.Field(proto.INT32, number=1) + + cols = proto.Field(proto.INT32, number=2) + + type_ = proto.Field(proto.INT32, number=3) + + data = proto.Field(proto.BYTES, number=4) + + class Layout(proto.Message): + r"""Visual element describing a layout unit on a page. + + Attributes: + text_anchor (google.cloud.documentai_v1.types.Document.TextAnchor): + Text anchor indexing into the + [Document.text][google.cloud.documentai.v1.Document.text]. + confidence (float): + Confidence of the current + [Layout][google.cloud.documentai.v1.Document.Page.Layout] + within context of the object this layout is for. e.g. + confidence can be for a single token, a table, a visual + element, etc. depending on context. Range [0, 1]. + bounding_poly (google.cloud.documentai_v1.types.BoundingPoly): + The bounding polygon for the + [Layout][google.cloud.documentai.v1.Document.Page.Layout]. + orientation (google.cloud.documentai_v1.types.Document.Page.Layout.Orientation): + Detected orientation for the + [Layout][google.cloud.documentai.v1.Document.Page.Layout]. + """ + + class Orientation(proto.Enum): + r"""Detected human reading orientation.""" + ORIENTATION_UNSPECIFIED = 0 + PAGE_UP = 1 + PAGE_RIGHT = 2 + PAGE_DOWN = 3 + PAGE_LEFT = 4 + + text_anchor = proto.Field( + proto.MESSAGE, number=1, message="Document.TextAnchor", + ) + + confidence = proto.Field(proto.FLOAT, number=2) + + bounding_poly = proto.Field( + proto.MESSAGE, number=3, message=geometry.BoundingPoly, + ) + + orientation = proto.Field( + proto.ENUM, number=4, enum="Document.Page.Layout.Orientation", + ) + + class Block(proto.Message): + r"""A block has a set of lines (collected into paragraphs) that + have a common line-spacing and orientation. + + Attributes: + layout (google.cloud.documentai_v1.types.Document.Page.Layout): + [Layout][google.cloud.documentai.v1.Document.Page.Layout] + for [Block][google.cloud.documentai.v1.Document.Page.Block]. + detected_languages (Sequence[google.cloud.documentai_v1.types.Document.Page.DetectedLanguage]): + A list of detected languages together with + confidence. + provenance (google.cloud.documentai_v1.types.Document.Provenance): + The history of this annotation. + """ + + layout = proto.Field( + proto.MESSAGE, number=1, message="Document.Page.Layout", + ) + + detected_languages = proto.RepeatedField( + proto.MESSAGE, number=2, message="Document.Page.DetectedLanguage", + ) + + provenance = proto.Field( + proto.MESSAGE, number=3, message="Document.Provenance", + ) + + class Paragraph(proto.Message): + r"""A collection of lines that a human would perceive as a + paragraph. + + Attributes: + layout (google.cloud.documentai_v1.types.Document.Page.Layout): + [Layout][google.cloud.documentai.v1.Document.Page.Layout] + for + [Paragraph][google.cloud.documentai.v1.Document.Page.Paragraph]. + detected_languages (Sequence[google.cloud.documentai_v1.types.Document.Page.DetectedLanguage]): + A list of detected languages together with + confidence. + provenance (google.cloud.documentai_v1.types.Document.Provenance): + The history of this annotation. + """ + + layout = proto.Field( + proto.MESSAGE, number=1, message="Document.Page.Layout", + ) + + detected_languages = proto.RepeatedField( + proto.MESSAGE, number=2, message="Document.Page.DetectedLanguage", + ) + + provenance = proto.Field( + proto.MESSAGE, number=3, message="Document.Provenance", + ) + + class Line(proto.Message): + r"""A collection of tokens that a human would perceive as a line. + Does not cross column boundaries, can be horizontal, vertical, + etc. + + Attributes: + layout (google.cloud.documentai_v1.types.Document.Page.Layout): + [Layout][google.cloud.documentai.v1.Document.Page.Layout] + for [Line][google.cloud.documentai.v1.Document.Page.Line]. + detected_languages (Sequence[google.cloud.documentai_v1.types.Document.Page.DetectedLanguage]): + A list of detected languages together with + confidence. + provenance (google.cloud.documentai_v1.types.Document.Provenance): + The history of this annotation. + """ + + layout = proto.Field( + proto.MESSAGE, number=1, message="Document.Page.Layout", + ) + + detected_languages = proto.RepeatedField( + proto.MESSAGE, number=2, message="Document.Page.DetectedLanguage", + ) + + provenance = proto.Field( + proto.MESSAGE, number=3, message="Document.Provenance", + ) + + class Token(proto.Message): + r"""A detected token. + + Attributes: + layout (google.cloud.documentai_v1.types.Document.Page.Layout): + [Layout][google.cloud.documentai.v1.Document.Page.Layout] + for [Token][google.cloud.documentai.v1.Document.Page.Token]. + detected_break (google.cloud.documentai_v1.types.Document.Page.Token.DetectedBreak): + Detected break at the end of a + [Token][google.cloud.documentai.v1.Document.Page.Token]. + detected_languages (Sequence[google.cloud.documentai_v1.types.Document.Page.DetectedLanguage]): + A list of detected languages together with + confidence. + provenance (google.cloud.documentai_v1.types.Document.Provenance): + The history of this annotation. + """ + + class DetectedBreak(proto.Message): + r"""Detected break at the end of a + [Token][google.cloud.documentai.v1.Document.Page.Token]. + + Attributes: + type_ (google.cloud.documentai_v1.types.Document.Page.Token.DetectedBreak.Type): + Detected break type. + """ + + class Type(proto.Enum): + r"""Enum to denote the type of break found.""" + TYPE_UNSPECIFIED = 0 + SPACE = 1 + WIDE_SPACE = 2 + HYPHEN = 3 + + type_ = proto.Field( + proto.ENUM, number=1, enum="Document.Page.Token.DetectedBreak.Type", + ) + + layout = proto.Field( + proto.MESSAGE, number=1, message="Document.Page.Layout", + ) + + detected_break = proto.Field( + proto.MESSAGE, number=2, message="Document.Page.Token.DetectedBreak", + ) + + detected_languages = proto.RepeatedField( + proto.MESSAGE, number=3, message="Document.Page.DetectedLanguage", + ) + + provenance = proto.Field( + proto.MESSAGE, number=4, message="Document.Provenance", + ) + + class VisualElement(proto.Message): + r"""Detected non-text visual elements e.g. checkbox, signature + etc. on the page. + + Attributes: + layout (google.cloud.documentai_v1.types.Document.Page.Layout): + [Layout][google.cloud.documentai.v1.Document.Page.Layout] + for + [VisualElement][google.cloud.documentai.v1.Document.Page.VisualElement]. + type_ (str): + Type of the + [VisualElement][google.cloud.documentai.v1.Document.Page.VisualElement]. + detected_languages (Sequence[google.cloud.documentai_v1.types.Document.Page.DetectedLanguage]): + A list of detected languages together with + confidence. + """ + + layout = proto.Field( + proto.MESSAGE, number=1, message="Document.Page.Layout", + ) + + type_ = proto.Field(proto.STRING, number=2) + + detected_languages = proto.RepeatedField( + proto.MESSAGE, number=3, message="Document.Page.DetectedLanguage", + ) + + class Table(proto.Message): + r"""A table representation similar to HTML table structure. + + Attributes: + layout (google.cloud.documentai_v1.types.Document.Page.Layout): + [Layout][google.cloud.documentai.v1.Document.Page.Layout] + for [Table][google.cloud.documentai.v1.Document.Page.Table]. + header_rows (Sequence[google.cloud.documentai_v1.types.Document.Page.Table.TableRow]): + Header rows of the table. + body_rows (Sequence[google.cloud.documentai_v1.types.Document.Page.Table.TableRow]): + Body rows of the table. + detected_languages (Sequence[google.cloud.documentai_v1.types.Document.Page.DetectedLanguage]): + A list of detected languages together with + confidence. + """ + + class TableRow(proto.Message): + r"""A row of table cells. + + Attributes: + cells (Sequence[google.cloud.documentai_v1.types.Document.Page.Table.TableCell]): + Cells that make up this row. + """ + + cells = proto.RepeatedField( + proto.MESSAGE, number=1, message="Document.Page.Table.TableCell", + ) + + class TableCell(proto.Message): + r"""A cell representation inside the table. + + Attributes: + layout (google.cloud.documentai_v1.types.Document.Page.Layout): + [Layout][google.cloud.documentai.v1.Document.Page.Layout] + for + [TableCell][google.cloud.documentai.v1.Document.Page.Table.TableCell]. + row_span (int): + How many rows this cell spans. + col_span (int): + How many columns this cell spans. + detected_languages (Sequence[google.cloud.documentai_v1.types.Document.Page.DetectedLanguage]): + A list of detected languages together with + confidence. + """ + + layout = proto.Field( + proto.MESSAGE, number=1, message="Document.Page.Layout", + ) + + row_span = proto.Field(proto.INT32, number=2) + + col_span = proto.Field(proto.INT32, number=3) + + detected_languages = proto.RepeatedField( + proto.MESSAGE, number=4, message="Document.Page.DetectedLanguage", + ) + + layout = proto.Field( + proto.MESSAGE, number=1, message="Document.Page.Layout", + ) + + header_rows = proto.RepeatedField( + proto.MESSAGE, number=2, message="Document.Page.Table.TableRow", + ) + + body_rows = proto.RepeatedField( + proto.MESSAGE, number=3, message="Document.Page.Table.TableRow", + ) + + detected_languages = proto.RepeatedField( + proto.MESSAGE, number=4, message="Document.Page.DetectedLanguage", + ) + + class FormField(proto.Message): + r"""A form field detected on the page. + + Attributes: + field_name (google.cloud.documentai_v1.types.Document.Page.Layout): + [Layout][google.cloud.documentai.v1.Document.Page.Layout] + for the + [FormField][google.cloud.documentai.v1.Document.Page.FormField] + name. e.g. ``Address``, ``Email``, ``Grand total``, + ``Phone number``, etc. + field_value (google.cloud.documentai_v1.types.Document.Page.Layout): + [Layout][google.cloud.documentai.v1.Document.Page.Layout] + for the + [FormField][google.cloud.documentai.v1.Document.Page.FormField] + value. + name_detected_languages (Sequence[google.cloud.documentai_v1.types.Document.Page.DetectedLanguage]): + A list of detected languages for name + together with confidence. + value_detected_languages (Sequence[google.cloud.documentai_v1.types.Document.Page.DetectedLanguage]): + A list of detected languages for value + together with confidence. + value_type (str): + If the value is non-textual, this field represents the type. + Current valid values are: + + - blank (this indicates the field_value is normal text) + - "unfilled_checkbox" + - "filled_checkbox". + """ + + field_name = proto.Field( + proto.MESSAGE, number=1, message="Document.Page.Layout", + ) + + field_value = proto.Field( + proto.MESSAGE, number=2, message="Document.Page.Layout", + ) + + name_detected_languages = proto.RepeatedField( + proto.MESSAGE, number=3, message="Document.Page.DetectedLanguage", + ) + + value_detected_languages = proto.RepeatedField( + proto.MESSAGE, number=4, message="Document.Page.DetectedLanguage", + ) + + value_type = proto.Field(proto.STRING, number=5) + + class DetectedLanguage(proto.Message): + r"""Detected language for a structural component. + + Attributes: + language_code (str): + The BCP-47 language code, such as "en-US" or "sr-Latn". For + more information, see + http://www.unicode.org/reports/tr35/#Unicode_locale_identifier. + confidence (float): + Confidence of detected language. Range [0, 1]. + """ + + language_code = proto.Field(proto.STRING, number=1) + + confidence = proto.Field(proto.FLOAT, number=2) + + page_number = proto.Field(proto.INT32, number=1) + + image = proto.Field(proto.MESSAGE, number=13, message="Document.Page.Image",) + + transforms = proto.RepeatedField( + proto.MESSAGE, number=14, message="Document.Page.Matrix", + ) + + dimension = proto.Field( + proto.MESSAGE, number=2, message="Document.Page.Dimension", + ) + + layout = proto.Field(proto.MESSAGE, number=3, message="Document.Page.Layout",) + + detected_languages = proto.RepeatedField( + proto.MESSAGE, number=4, message="Document.Page.DetectedLanguage", + ) + + blocks = proto.RepeatedField( + proto.MESSAGE, number=5, message="Document.Page.Block", + ) + + paragraphs = proto.RepeatedField( + proto.MESSAGE, number=6, message="Document.Page.Paragraph", + ) + + lines = proto.RepeatedField( + proto.MESSAGE, number=7, message="Document.Page.Line", + ) + + tokens = proto.RepeatedField( + proto.MESSAGE, number=8, message="Document.Page.Token", + ) + + visual_elements = proto.RepeatedField( + proto.MESSAGE, number=9, message="Document.Page.VisualElement", + ) + + tables = proto.RepeatedField( + proto.MESSAGE, number=10, message="Document.Page.Table", + ) + + form_fields = proto.RepeatedField( + proto.MESSAGE, number=11, message="Document.Page.FormField", + ) + + class Entity(proto.Message): + r"""A phrase in the text that is a known entity type, such as a + person, an organization, or location. + + Attributes: + text_anchor (google.cloud.documentai_v1.types.Document.TextAnchor): + Optional. Provenance of the entity. Text anchor indexing + into the + [Document.text][google.cloud.documentai.v1.Document.text]. + type_ (str): + Entity type from a schema e.g. ``Address``. + mention_text (str): + Optional. Text value in the document e.g. + ``1600 Amphitheatre Pkwy``. + mention_id (str): + Optional. Deprecated. Use ``id`` field instead. + confidence (float): + Optional. Confidence of detected Schema entity. Range [0, + 1]. + page_anchor (google.cloud.documentai_v1.types.Document.PageAnchor): + Optional. Represents the provenance of this + entity wrt. the location on the page where it + was found. + id (str): + Optional. Canonical id. This will be a unique + value in the entity list for this document. + normalized_value (google.cloud.documentai_v1.types.Document.Entity.NormalizedValue): + Optional. Normalized entity value. Absent if + the extracted value could not be converted or + the type (e.g. address) is not supported for + certain parsers. This field is also only + populated for certain supported document types. + properties (Sequence[google.cloud.documentai_v1.types.Document.Entity]): + Optional. Entities can be nested to form a + hierarchical data structure representing the + content in the document. + provenance (google.cloud.documentai_v1.types.Document.Provenance): + Optional. The history of this annotation. + redacted (bool): + Optional. Whether the entity will be redacted + for de-identification purposes. + """ + + class NormalizedValue(proto.Message): + r"""Parsed and normalized entity value. + + Attributes: + money_value (google.type.money_pb2.Money): + Money value. See also: + https://github.com/googleapis/googleapis/blob/master/google/type/money.proto + date_value (google.type.date_pb2.Date): + Date value. Includes year, month, day. See + also: + https://github.com/googleapis/googleapis/blob/master/google/type/date.proto + datetime_value (google.type.datetime_pb2.DateTime): + DateTime value. Includes date, time, and + timezone. See also: + https://github.com/googleapis/googleapis/blob/master/google/type/datetime.proto + address_value (google.type.postal_address_pb2.PostalAddress): + Postal address. See also: + https://github.com/googleapis/googleapis/blob/master/google/type/postal_address.proto + boolean_value (bool): + Boolean value. Can be used for entities with + binary values, or for checkboxes. + text (str): + Required. Normalized entity value stored as a string. This + field is populated for supported document type (e.g. + Invoice). For some entity types, one of respective + 'structured_value' fields may also be populated. + + - Money/Currency type (``money_value``) is in the ISO 4217 + text format. + - Date type (``date_value``) is in the ISO 8601 text + format. + - Datetime type (``datetime_value``) is in the ISO 8601 + text format. + """ + + money_value = proto.Field( + proto.MESSAGE, number=2, oneof="structured_value", message=money.Money, + ) + + date_value = proto.Field( + proto.MESSAGE, number=3, oneof="structured_value", message=date.Date, + ) + + datetime_value = proto.Field( + proto.MESSAGE, + number=4, + oneof="structured_value", + message=datetime.DateTime, + ) + + address_value = proto.Field( + proto.MESSAGE, + number=5, + oneof="structured_value", + message=postal_address.PostalAddress, + ) + + boolean_value = proto.Field(proto.BOOL, number=6, oneof="structured_value") + + text = proto.Field(proto.STRING, number=1) + + text_anchor = proto.Field( + proto.MESSAGE, number=1, message="Document.TextAnchor", + ) + + type_ = proto.Field(proto.STRING, number=2) + + mention_text = proto.Field(proto.STRING, number=3) + + mention_id = proto.Field(proto.STRING, number=4) + + confidence = proto.Field(proto.FLOAT, number=5) + + page_anchor = proto.Field( + proto.MESSAGE, number=6, message="Document.PageAnchor", + ) + + id = proto.Field(proto.STRING, number=7) + + normalized_value = proto.Field( + proto.MESSAGE, number=9, message="Document.Entity.NormalizedValue", + ) + + properties = proto.RepeatedField( + proto.MESSAGE, number=10, message="Document.Entity", + ) + + provenance = proto.Field( + proto.MESSAGE, number=11, message="Document.Provenance", + ) + + redacted = proto.Field(proto.BOOL, number=12) + + class EntityRelation(proto.Message): + r"""Relationship between + [Entities][google.cloud.documentai.v1.Document.Entity]. + + Attributes: + subject_id (str): + Subject entity id. + object_id (str): + Object entity id. + relation (str): + Relationship description. + """ + + subject_id = proto.Field(proto.STRING, number=1) + + object_id = proto.Field(proto.STRING, number=2) + + relation = proto.Field(proto.STRING, number=3) + + class TextAnchor(proto.Message): + r"""Text reference indexing into the + [Document.text][google.cloud.documentai.v1.Document.text]. + + Attributes: + text_segments (Sequence[google.cloud.documentai_v1.types.Document.TextAnchor.TextSegment]): + The text segments from the + [Document.text][google.cloud.documentai.v1.Document.text]. + content (str): + Contains the content of the text span so that users do not + have to look it up in the text_segments. + """ + + class TextSegment(proto.Message): + r"""A text segment in the + [Document.text][google.cloud.documentai.v1.Document.text]. The + indices may be out of bounds which indicate that the text extends + into another document shard for large sharded documents. See + [ShardInfo.text_offset][google.cloud.documentai.v1.Document.ShardInfo.text_offset] + + Attributes: + start_index (int): + [TextSegment][google.cloud.documentai.v1.Document.TextAnchor.TextSegment] + start UTF-8 char index in the + [Document.text][google.cloud.documentai.v1.Document.text]. + end_index (int): + [TextSegment][google.cloud.documentai.v1.Document.TextAnchor.TextSegment] + half open end UTF-8 char index in the + [Document.text][google.cloud.documentai.v1.Document.text]. + """ + + start_index = proto.Field(proto.INT64, number=1) + + end_index = proto.Field(proto.INT64, number=2) + + text_segments = proto.RepeatedField( + proto.MESSAGE, number=1, message="Document.TextAnchor.TextSegment", + ) + + content = proto.Field(proto.STRING, number=2) + + class PageAnchor(proto.Message): + r"""Referencing the visual context of the entity in the + [Document.pages][google.cloud.documentai.v1.Document.pages]. Page + anchors can be cross-page, consist of multiple bounding polygons and + optionally reference specific layout element types. + + Attributes: + page_refs (Sequence[google.cloud.documentai_v1.types.Document.PageAnchor.PageRef]): + One or more references to visual page + elements + """ + + class PageRef(proto.Message): + r"""Represents a weak reference to a page element within a + document. + + Attributes: + page (int): + Required. Index into the + [Document.pages][google.cloud.documentai.v1.Document.pages] + element, for example using [Document.pages][page_refs.page] + to locate the related page element. + layout_type (google.cloud.documentai_v1.types.Document.PageAnchor.PageRef.LayoutType): + Optional. The type of the layout element that + is being referenced if any. + layout_id (str): + Optional. Deprecated. Use + [PageRef.bounding_poly][google.cloud.documentai.v1.Document.PageAnchor.PageRef.bounding_poly] + instead. + bounding_poly (google.cloud.documentai_v1.types.BoundingPoly): + Optional. Identifies the bounding polygon of + a layout element on the page. + """ + + class LayoutType(proto.Enum): + r"""The type of layout that is being referenced.""" + LAYOUT_TYPE_UNSPECIFIED = 0 + BLOCK = 1 + PARAGRAPH = 2 + LINE = 3 + TOKEN = 4 + VISUAL_ELEMENT = 5 + TABLE = 6 + FORM_FIELD = 7 + + page = proto.Field(proto.INT64, number=1) + + layout_type = proto.Field( + proto.ENUM, number=2, enum="Document.PageAnchor.PageRef.LayoutType", + ) + + layout_id = proto.Field(proto.STRING, number=3) + + bounding_poly = proto.Field( + proto.MESSAGE, number=4, message=geometry.BoundingPoly, + ) + + page_refs = proto.RepeatedField( + proto.MESSAGE, number=1, message="Document.PageAnchor.PageRef", + ) + + class Provenance(proto.Message): + r"""Structure to identify provenance relationships between + annotations in different revisions. + + Attributes: + revision (int): + The index of the revision that produced this + element. + id (int): + The Id of this operation. Needs to be unique + within the scope of the revision. + parents (Sequence[google.cloud.documentai_v1.types.Document.Provenance.Parent]): + References to the original elements that are + replaced. + type_ (google.cloud.documentai_v1.types.Document.Provenance.OperationType): + The type of provenance operation. + """ + + class OperationType(proto.Enum): + r"""If a processor or agent does an explicit operation on + existing elements. + """ + OPERATION_TYPE_UNSPECIFIED = 0 + ADD = 1 + REMOVE = 2 + REPLACE = 3 + EVAL_REQUESTED = 4 + EVAL_APPROVED = 5 + EVAL_SKIPPED = 6 + + class Parent(proto.Message): + r"""Structure for referencing parent provenances. When an + element replaces one of more other elements parent references + identify the elements that are replaced. + + Attributes: + revision (int): + The index of the [Document.revisions] identifying the parent + revision. + id (int): + The id of the parent provenance. + """ + + revision = proto.Field(proto.INT32, number=1) + + id = proto.Field(proto.INT32, number=2) + + revision = proto.Field(proto.INT32, number=1) + + id = proto.Field(proto.INT32, number=2) + + parents = proto.RepeatedField( + proto.MESSAGE, number=3, message="Document.Provenance.Parent", + ) + + type_ = proto.Field( + proto.ENUM, number=4, enum="Document.Provenance.OperationType", + ) + + class Revision(proto.Message): + r"""Contains past or forward revisions of this document. + + Attributes: + agent (str): + If the change was made by a person specify + the name or id of that person. + processor (str): + If the annotation was made by processor + identify the processor by its resource name. + id (str): + Id of the revision. Unique within the + context of the document. + parent (Sequence[int]): + The revisions that this revision is based on. This can + include one or more parent (when documents are merged.) This + field represents the index into the ``revisions`` field. + create_time (google.protobuf.timestamp_pb2.Timestamp): + The time that the revision was created. + human_review (google.cloud.documentai_v1.types.Document.Revision.HumanReview): + Human Review information of this revision. + """ + + class HumanReview(proto.Message): + r"""Human Review information of the document. + + Attributes: + state (str): + Human review state. e.g. ``requested``, ``succeeded``, + ``rejected``. + state_message (str): + A message providing more details about the current state of + processing. For example, the rejection reason when the state + is ``rejected``. + """ + + state = proto.Field(proto.STRING, number=1) + + state_message = proto.Field(proto.STRING, number=2) + + agent = proto.Field(proto.STRING, number=4, oneof="source") + + processor = proto.Field(proto.STRING, number=5, oneof="source") + + id = proto.Field(proto.STRING, number=1) + + parent = proto.RepeatedField(proto.INT32, number=2) + + create_time = proto.Field(proto.MESSAGE, number=3, message=timestamp.Timestamp,) + + human_review = proto.Field( + proto.MESSAGE, number=6, message="Document.Revision.HumanReview", + ) + + class TextChange(proto.Message): + r"""This message is used for text changes aka. OCR corrections. + + Attributes: + text_anchor (google.cloud.documentai_v1.types.Document.TextAnchor): + Provenance of the correction. Text anchor indexing into the + [Document.text][google.cloud.documentai.v1.Document.text]. + There can only be a single ``TextAnchor.text_segments`` + element. If the start and end index of the text segment are + the same, the text change is inserted before that index. + changed_text (str): + The text that replaces the text identified in the + ``text_anchor``. + provenance (Sequence[google.cloud.documentai_v1.types.Document.Provenance]): + The history of this annotation. + """ + + text_anchor = proto.Field( + proto.MESSAGE, number=1, message="Document.TextAnchor", + ) + + changed_text = proto.Field(proto.STRING, number=2) + + provenance = proto.RepeatedField( + proto.MESSAGE, number=3, message="Document.Provenance", + ) + + uri = proto.Field(proto.STRING, number=1, oneof="source") + + content = proto.Field(proto.BYTES, number=2, oneof="source") + + mime_type = proto.Field(proto.STRING, number=3) + + text = proto.Field(proto.STRING, number=4) + + text_styles = proto.RepeatedField(proto.MESSAGE, number=5, message=Style,) + + pages = proto.RepeatedField(proto.MESSAGE, number=6, message=Page,) + + entities = proto.RepeatedField(proto.MESSAGE, number=7, message=Entity,) + + entity_relations = proto.RepeatedField( + proto.MESSAGE, number=8, message=EntityRelation, + ) + + text_changes = proto.RepeatedField(proto.MESSAGE, number=14, message=TextChange,) + + shard_info = proto.Field(proto.MESSAGE, number=9, message=ShardInfo,) + + error = proto.Field(proto.MESSAGE, number=10, message=status.Status,) + + revisions = proto.RepeatedField(proto.MESSAGE, number=13, message=Revision,) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/documentai_v1/types/document_io.py b/google/cloud/documentai_v1/types/document_io.py new file mode 100644 index 00000000..50196830 --- /dev/null +++ b/google/cloud/documentai_v1/types/document_io.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +# 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 proto # type: ignore + + +__protobuf__ = proto.module( + package="google.cloud.documentai.v1", + manifest={ + "RawDocument", + "GcsDocument", + "GcsDocuments", + "GcsPrefix", + "BatchDocumentsInputConfig", + "DocumentOutputConfig", + }, +) + + +class RawDocument(proto.Message): + r"""Payload message of raw document content (bytes). + + Attributes: + content (bytes): + Inline document content. + mime_type (str): + An IANA MIME type (RFC6838) indicating the nature and format + of the [content]. + """ + + content = proto.Field(proto.BYTES, number=1) + + mime_type = proto.Field(proto.STRING, number=2) + + +class GcsDocument(proto.Message): + r"""Specifies a document stored on Cloud Storage. + + Attributes: + gcs_uri (str): + The Cloud Storage object uri. + mime_type (str): + An IANA MIME type (RFC6838) of the content. + """ + + gcs_uri = proto.Field(proto.STRING, number=1) + + mime_type = proto.Field(proto.STRING, number=2) + + +class GcsDocuments(proto.Message): + r"""Specifies a set of documents on Cloud Storage. + + Attributes: + documents (Sequence[google.cloud.documentai_v1.types.GcsDocument]): + The list of documents. + """ + + documents = proto.RepeatedField(proto.MESSAGE, number=1, message="GcsDocument",) + + +class GcsPrefix(proto.Message): + r"""Specifies all documents on Cloud Storage with a common + prefix. + + Attributes: + gcs_uri_prefix (str): + The URI prefix. + """ + + gcs_uri_prefix = proto.Field(proto.STRING, number=1) + + +class BatchDocumentsInputConfig(proto.Message): + r"""The common config to specify a set of documents used as + input. + + Attributes: + gcs_prefix (google.cloud.documentai_v1.types.GcsPrefix): + The set of documents that match the specified Cloud Storage + [gcs_prefix]. + gcs_documents (google.cloud.documentai_v1.types.GcsDocuments): + The set of documents individually specified + on Cloud Storage. + """ + + gcs_prefix = proto.Field( + proto.MESSAGE, number=1, oneof="source", message="GcsPrefix", + ) + + gcs_documents = proto.Field( + proto.MESSAGE, number=2, oneof="source", message="GcsDocuments", + ) + + +class DocumentOutputConfig(proto.Message): + r"""Config that controls the output of documents. All documents + will be written as a JSON file. + + Attributes: + gcs_output_config (google.cloud.documentai_v1.types.DocumentOutputConfig.GcsOutputConfig): + Output config to write the results to Cloud + Storage. + """ + + class GcsOutputConfig(proto.Message): + r"""The configuration used when outputting documents. + + Attributes: + gcs_uri (str): + The Cloud Storage uri (a directory) of the + output. + """ + + gcs_uri = proto.Field(proto.STRING, number=1) + + gcs_output_config = proto.Field( + proto.MESSAGE, number=1, oneof="destination", message=GcsOutputConfig, + ) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/documentai_v1/types/document_processor_service.py b/google/cloud/documentai_v1/types/document_processor_service.py new file mode 100644 index 00000000..cfdcc7f5 --- /dev/null +++ b/google/cloud/documentai_v1/types/document_processor_service.py @@ -0,0 +1,309 @@ +# -*- coding: utf-8 -*- + +# 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 proto # type: ignore + + +from google.cloud.documentai_v1.types import document as gcd_document +from google.cloud.documentai_v1.types import document_io +from google.protobuf import timestamp_pb2 as timestamp # type: ignore +from google.rpc import status_pb2 as gr_status # type: ignore + + +__protobuf__ = proto.module( + package="google.cloud.documentai.v1", + manifest={ + "ProcessRequest", + "HumanReviewStatus", + "ProcessResponse", + "BatchProcessRequest", + "BatchProcessResponse", + "BatchProcessMetadata", + "ReviewDocumentRequest", + "ReviewDocumentResponse", + "ReviewDocumentOperationMetadata", + "CommonOperationMetadata", + }, +) + + +class ProcessRequest(proto.Message): + r"""Request message for the process document method. + + Attributes: + inline_document (google.cloud.documentai_v1.types.Document): + An inline document proto. + raw_document (google.cloud.documentai_v1.types.RawDocument): + A raw document content (bytes). + name (str): + Required. The processor resource name. + skip_human_review (bool): + Whether Human Review feature should be + skipped for this request. Default to false. + """ + + inline_document = proto.Field( + proto.MESSAGE, number=4, oneof="source", message=gcd_document.Document, + ) + + raw_document = proto.Field( + proto.MESSAGE, number=5, oneof="source", message=document_io.RawDocument, + ) + + name = proto.Field(proto.STRING, number=1) + + skip_human_review = proto.Field(proto.BOOL, number=3) + + +class HumanReviewStatus(proto.Message): + r"""The status of human review on a processed document. + + Attributes: + state (google.cloud.documentai_v1.types.HumanReviewStatus.State): + The state of human review on the processing + request. + state_message (str): + A message providing more details about the + human review state. + human_review_operation (str): + The name of the operation triggered by the processed + document. This field is populated only when the [state] is + [HUMAN_REVIEW_IN_PROGRESS]. It has the same response type + and metadata as the long running operation returned by + [ReviewDocument] method. + """ + + class State(proto.Enum): + r"""The final state of human review on a processed document.""" + STATE_UNSPECIFIED = 0 + SKIPPED = 1 + VALIDATION_PASSED = 2 + IN_PROGRESS = 3 + ERROR = 4 + + state = proto.Field(proto.ENUM, number=1, enum=State,) + + state_message = proto.Field(proto.STRING, number=2) + + human_review_operation = proto.Field(proto.STRING, number=3) + + +class ProcessResponse(proto.Message): + r"""Response message for the process document method. + + Attributes: + document (google.cloud.documentai_v1.types.Document): + The document payload, will populate fields + based on the processor's behavior. + human_review_status (google.cloud.documentai_v1.types.HumanReviewStatus): + The status of human review on the processed + document. + """ + + document = proto.Field(proto.MESSAGE, number=1, message=gcd_document.Document,) + + human_review_status = proto.Field( + proto.MESSAGE, number=3, message="HumanReviewStatus", + ) + + +class BatchProcessRequest(proto.Message): + r"""Request message for batch process document method. + + Attributes: + name (str): + Required. The processor resource name. + input_documents (google.cloud.documentai_v1.types.BatchDocumentsInputConfig): + The input documents for batch process. + document_output_config (google.cloud.documentai_v1.types.DocumentOutputConfig): + The overall output config for batch process. + skip_human_review (bool): + Whether Human Review feature should be + skipped for this request. Default to false. + """ + + name = proto.Field(proto.STRING, number=1) + + input_documents = proto.Field( + proto.MESSAGE, number=5, message=document_io.BatchDocumentsInputConfig, + ) + + document_output_config = proto.Field( + proto.MESSAGE, number=6, message=document_io.DocumentOutputConfig, + ) + + skip_human_review = proto.Field(proto.BOOL, number=4) + + +class BatchProcessResponse(proto.Message): + r"""Response message for batch process document method.""" + + +class BatchProcessMetadata(proto.Message): + r"""The long running operation metadata for batch process method. + + Attributes: + state (google.cloud.documentai_v1.types.BatchProcessMetadata.State): + The state of the current batch processing. + state_message (str): + A message providing more details about the + current state of processing. For example, the + error message if the operation is failed. + create_time (google.protobuf.timestamp_pb2.Timestamp): + The creation time of the operation. + update_time (google.protobuf.timestamp_pb2.Timestamp): + The last update time of the operation. + individual_process_statuses (Sequence[google.cloud.documentai_v1.types.BatchProcessMetadata.IndividualProcessStatus]): + The list of response details of each + document. + """ + + class State(proto.Enum): + r"""Possible states of the batch processing operation.""" + STATE_UNSPECIFIED = 0 + WAITING = 1 + RUNNING = 2 + SUCCEEDED = 3 + CANCELLING = 4 + CANCELLED = 5 + FAILED = 6 + + class IndividualProcessStatus(proto.Message): + r"""The status of a each individual document in the batch + process. + + Attributes: + input_gcs_source (str): + The source of the document, same as the [input_gcs_source] + field in the request when the batch process started. The + batch process is started by take snapshot of that document, + since a user can move or change that document during the + process. + status (google.rpc.status_pb2.Status): + The status of the processing of the document. + output_gcs_destination (str): + The output_gcs_destination (in the request as + 'output_gcs_destination') of the processed document if it + was successful, otherwise empty. + human_review_status (google.cloud.documentai_v1.types.HumanReviewStatus): + The status of human review on the processed + document. + """ + + input_gcs_source = proto.Field(proto.STRING, number=1) + + status = proto.Field(proto.MESSAGE, number=2, message=gr_status.Status,) + + output_gcs_destination = proto.Field(proto.STRING, number=3) + + human_review_status = proto.Field( + proto.MESSAGE, number=5, message="HumanReviewStatus", + ) + + state = proto.Field(proto.ENUM, number=1, enum=State,) + + state_message = proto.Field(proto.STRING, number=2) + + create_time = proto.Field(proto.MESSAGE, number=3, message=timestamp.Timestamp,) + + update_time = proto.Field(proto.MESSAGE, number=4, message=timestamp.Timestamp,) + + individual_process_statuses = proto.RepeatedField( + proto.MESSAGE, number=5, message=IndividualProcessStatus, + ) + + +class ReviewDocumentRequest(proto.Message): + r"""Request message for review document method. + + Attributes: + inline_document (google.cloud.documentai_v1.types.Document): + An inline document proto. + human_review_config (str): + Required. The resource name of the + HumanReviewConfig that the document will be + reviewed with. + """ + + inline_document = proto.Field( + proto.MESSAGE, number=4, oneof="source", message=gcd_document.Document, + ) + + human_review_config = proto.Field(proto.STRING, number=1) + + +class ReviewDocumentResponse(proto.Message): + r"""Response message for review document method. + + Attributes: + gcs_destination (str): + The Cloud Storage uri for the human reviewed + document. + """ + + gcs_destination = proto.Field(proto.STRING, number=1) + + +class ReviewDocumentOperationMetadata(proto.Message): + r"""The long running operation metadata for review document + method. + + Attributes: + common_metadata (google.cloud.documentai_v1.types.CommonOperationMetadata): + The basic metadata of the long running + operation. + """ + + common_metadata = proto.Field( + proto.MESSAGE, number=5, message="CommonOperationMetadata", + ) + + +class CommonOperationMetadata(proto.Message): + r"""The common metadata for long running operations. + + Attributes: + state (google.cloud.documentai_v1.types.CommonOperationMetadata.State): + The state of the operation. + state_message (str): + A message providing more details about the + current state of processing. + create_time (google.protobuf.timestamp_pb2.Timestamp): + The creation time of the operation. + update_time (google.protobuf.timestamp_pb2.Timestamp): + The last update time of the operation. + """ + + class State(proto.Enum): + r"""State of the longrunning operation.""" + STATE_UNSPECIFIED = 0 + RUNNING = 1 + CANCELLING = 2 + SUCCEEDED = 3 + FAILED = 4 + CANCELLED = 5 + + state = proto.Field(proto.ENUM, number=1, enum=State,) + + state_message = proto.Field(proto.STRING, number=2) + + create_time = proto.Field(proto.MESSAGE, number=3, message=timestamp.Timestamp,) + + update_time = proto.Field(proto.MESSAGE, number=4, message=timestamp.Timestamp,) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/documentai_v1/types/geometry.py b/google/cloud/documentai_v1/types/geometry.py new file mode 100644 index 00000000..3b3258ca --- /dev/null +++ b/google/cloud/documentai_v1/types/geometry.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +# 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 proto # type: ignore + + +__protobuf__ = proto.module( + package="google.cloud.documentai.v1", + manifest={"Vertex", "NormalizedVertex", "BoundingPoly",}, +) + + +class Vertex(proto.Message): + r"""A vertex represents a 2D point in the image. + NOTE: the vertex coordinates are in the same scale as the + original image. + + Attributes: + x (int): + X coordinate. + y (int): + Y coordinate. + """ + + x = proto.Field(proto.INT32, number=1) + + y = proto.Field(proto.INT32, number=2) + + +class NormalizedVertex(proto.Message): + r"""A vertex represents a 2D point in the image. + NOTE: the normalized vertex coordinates are relative to the + original image and range from 0 to 1. + + Attributes: + x (float): + X coordinate. + y (float): + Y coordinate. + """ + + x = proto.Field(proto.FLOAT, number=1) + + y = proto.Field(proto.FLOAT, number=2) + + +class BoundingPoly(proto.Message): + r"""A bounding polygon for the detected image annotation. + + Attributes: + vertices (Sequence[google.cloud.documentai_v1.types.Vertex]): + The bounding polygon vertices. + normalized_vertices (Sequence[google.cloud.documentai_v1.types.NormalizedVertex]): + The bounding polygon normalized vertices. + """ + + vertices = proto.RepeatedField(proto.MESSAGE, number=1, message="Vertex",) + + normalized_vertices = proto.RepeatedField( + proto.MESSAGE, number=2, message="NormalizedVertex", + ) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py index e57c3180..05afa3f5 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py @@ -85,12 +85,36 @@ class DocumentUnderstandingServiceAsyncClient: DocumentUnderstandingServiceClient.parse_common_location_path ) - from_service_account_info = ( - DocumentUnderstandingServiceClient.from_service_account_info - ) - from_service_account_file = ( - DocumentUnderstandingServiceClient.from_service_account_file - ) + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + DocumentUnderstandingServiceAsyncClient: The constructed client. + """ + return DocumentUnderstandingServiceClient.from_service_account_info.__func__(DocumentUnderstandingServiceAsyncClient, info, *args, **kwargs) # type: ignore + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + DocumentUnderstandingServiceAsyncClient: The constructed client. + """ + return DocumentUnderstandingServiceClient.from_service_account_file.__func__(DocumentUnderstandingServiceAsyncClient, filename, *args, **kwargs) # type: ignore + from_service_account_json = from_service_account_file @property @@ -223,6 +247,7 @@ async def batch_process_documents( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=120.0, ), default_timeout=120.0, client_info=DEFAULT_CLIENT_INFO, @@ -294,6 +319,7 @@ async def process_document( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=120.0, ), default_timeout=120.0, client_info=DEFAULT_CLIENT_INFO, diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py index 86e21191..9b8a6edd 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/client.py @@ -398,8 +398,8 @@ def batch_process_documents( # If we have keyword arguments corresponding to fields on the # request, apply these. - if requests: - request.requests.extend(requests) + if requests is not None: + request.requests = requests # Wrap the RPC method; this adds retry and timeout information, # and friendly error handling. diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/base.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/base.py index 547c5803..38db3690 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/base.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/base.py @@ -72,10 +72,10 @@ 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 + 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. @@ -83,6 +83,9 @@ def __init__( host += ":443" self._host = host + # Save the scopes. + self._scopes = scopes or self.AUTH_SCOPES + # If no credentials are provided, then determine the appropriate # defaults. if credentials and credentials_file: @@ -92,20 +95,17 @@ def __init__( if credentials_file is not None: credentials, _ = auth.load_credentials_from_file( - credentials_file, scopes=scopes, quota_project_id=quota_project_id + credentials_file, scopes=self._scopes, quota_project_id=quota_project_id ) elif credentials is None: credentials, _ = auth.default( - scopes=scopes, quota_project_id=quota_project_id + scopes=self._scopes, quota_project_id=quota_project_id ) # Save the credentials. self._credentials = credentials - # Lifted into its own function so it can be stubbed out during tests. - self._prep_wrapped_messages(client_info) - def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. self._wrapped_methods = { @@ -118,6 +118,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=120.0, ), default_timeout=120.0, client_info=client_info, @@ -131,6 +132,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=120.0, ), default_timeout=120.0, client_info=client_info, diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py index c0c57120..391fb597 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py @@ -111,7 +111,10 @@ def __init__( google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` and ``credentials_file`` are passed. """ + self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + self._operations_client = None if api_mtls_endpoint: warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) @@ -119,70 +122,50 @@ def __init__( warnings.warn("client_cert_source is deprecated", DeprecationWarning) if channel: - # Sanity check: Ensure that channel and credentials are not both - # provided. + # Ignore credentials if a channel was passed. credentials = False - # If a channel was explicitly provided, set it. self._grpc_channel = channel self._ssl_channel_credentials = None - elif api_mtls_endpoint: - 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: - cert, key = client_cert_source() - ssl_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) - else: - ssl_credentials = SslCredentials().ssl_credentials - # 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_credentials, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - options=[ - ("grpc.max_send_message_length", -1), - ("grpc.max_receive_message_length", -1), - ], - ) - self._ssl_channel_credentials = ssl_credentials else: - host = host if ":" in host else host + ":443" + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials - if credentials is None: - credentials, _ = auth.default( - scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id - ) + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) - if client_cert_source_for_mtls and not ssl_channel_credentials: - cert, key = client_cert_source_for_mtls() - self._ssl_channel_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + ) - # create a new channel. The provided one is ignored. + if not self._grpc_channel: self._grpc_channel = type(self).create_channel( - host, - credentials=credentials, + self._host, + credentials=self._credentials, credentials_file=credentials_file, + scopes=self._scopes, ssl_credentials=self._ssl_channel_credentials, - scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, options=[ ("grpc.max_send_message_length", -1), @@ -190,18 +173,8 @@ def __init__( ], ) - self._stubs = {} # type: Dict[str, Callable] - self._operations_client = None - - # Run the base constructor. - super().__init__( - host=host, - credentials=credentials, - credentials_file=credentials_file, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - client_info=client_info, - ) + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) @classmethod def create_channel( @@ -215,7 +188,7 @@ def create_channel( ) -> grpc.Channel: """Create and return a gRPC channel object. Args: - address (Optional[str]): The host for the channel to use. + host (Optional[str]): The host for the channel to use. credentials (Optional[~.Credentials]): The authorization credentials to attach to requests. These credentials identify this application to the service. If diff --git a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py index 7ac8f880..76cf0816 100644 --- a/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py +++ b/google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py @@ -68,7 +68,7 @@ def create_channel( ) -> aio.Channel: """Create and return a gRPC AsyncIO channel object. Args: - address (Optional[str]): The host for the channel to use. + host (Optional[str]): The host for the channel to use. credentials (Optional[~.Credentials]): The authorization credentials to attach to requests. These credentials identify this application to the service. If @@ -146,10 +146,10 @@ def __init__( ignored if ``channel`` or ``ssl_channel_credentials`` 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 + 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: @@ -158,7 +158,10 @@ def __init__( google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` and ``credentials_file`` are passed. """ + self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + self._operations_client = None if api_mtls_endpoint: warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) @@ -166,70 +169,50 @@ def __init__( warnings.warn("client_cert_source is deprecated", DeprecationWarning) if channel: - # Sanity check: Ensure that channel and credentials are not both - # provided. + # Ignore credentials if a channel was passed. credentials = False - # If a channel was explicitly provided, set it. self._grpc_channel = channel self._ssl_channel_credentials = None - elif api_mtls_endpoint: - 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: - cert, key = client_cert_source() - ssl_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) - else: - ssl_credentials = SslCredentials().ssl_credentials - # 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_credentials, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - options=[ - ("grpc.max_send_message_length", -1), - ("grpc.max_receive_message_length", -1), - ], - ) - self._ssl_channel_credentials = ssl_credentials else: - host = host if ":" in host else host + ":443" + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials - if credentials is None: - credentials, _ = auth.default( - scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id - ) + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) - if client_cert_source_for_mtls and not ssl_channel_credentials: - cert, key = client_cert_source_for_mtls() - self._ssl_channel_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + ) - # create a new channel. The provided one is ignored. + if not self._grpc_channel: self._grpc_channel = type(self).create_channel( - host, - credentials=credentials, + self._host, + credentials=self._credentials, credentials_file=credentials_file, + scopes=self._scopes, ssl_credentials=self._ssl_channel_credentials, - scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, options=[ ("grpc.max_send_message_length", -1), @@ -237,18 +220,8 @@ def __init__( ], ) - # Run the base constructor. - super().__init__( - host=host, - credentials=credentials, - credentials_file=credentials_file, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - client_info=client_info, - ) - - self._stubs = {} - self._operations_client = None + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) @property def grpc_channel(self) -> aio.Channel: diff --git a/google/cloud/documentai_v1beta2/types/__init__.py b/google/cloud/documentai_v1beta2/types/__init__.py index 73b83af3..e5578fac 100644 --- a/google/cloud/documentai_v1beta2/types/__init__.py +++ b/google/cloud/documentai_v1beta2/types/__init__.py @@ -15,50 +15,50 @@ # limitations under the License. # -from .geometry import ( - Vertex, - NormalizedVertex, - BoundingPoly, -) from .document import Document from .document_understanding import ( + AutoMlParams, BatchProcessDocumentsRequest, - ProcessDocumentRequest, BatchProcessDocumentsResponse, - ProcessDocumentResponse, - OcrParams, - TableExtractionParams, - TableBoundHint, - FormExtractionParams, - KeyValuePairHint, EntityExtractionParams, - AutoMlParams, - InputConfig, - OutputConfig, - GcsSource, + FormExtractionParams, GcsDestination, + GcsSource, + InputConfig, + KeyValuePairHint, + OcrParams, OperationMetadata, + OutputConfig, + ProcessDocumentRequest, + ProcessDocumentResponse, + TableBoundHint, + TableExtractionParams, +) +from .geometry import ( + BoundingPoly, + NormalizedVertex, + Vertex, ) __all__ = ( - "Vertex", - "NormalizedVertex", - "BoundingPoly", "Document", + "AutoMlParams", "BatchProcessDocumentsRequest", - "ProcessDocumentRequest", "BatchProcessDocumentsResponse", - "ProcessDocumentResponse", - "OcrParams", - "TableExtractionParams", - "TableBoundHint", - "FormExtractionParams", - "KeyValuePairHint", "EntityExtractionParams", - "AutoMlParams", - "InputConfig", - "OutputConfig", - "GcsSource", + "FormExtractionParams", "GcsDestination", + "GcsSource", + "InputConfig", + "KeyValuePairHint", + "OcrParams", "OperationMetadata", + "OutputConfig", + "ProcessDocumentRequest", + "ProcessDocumentResponse", + "TableBoundHint", + "TableExtractionParams", + "BoundingPoly", + "NormalizedVertex", + "Vertex", ) diff --git a/google/cloud/documentai_v1beta3/__init__.py b/google/cloud/documentai_v1beta3/__init__.py index c93f255b..84d917be 100644 --- a/google/cloud/documentai_v1beta3/__init__.py +++ b/google/cloud/documentai_v1beta3/__init__.py @@ -17,9 +17,17 @@ from .services.document_processor_service import DocumentProcessorServiceClient from .types.document import Document +from .types.document_io import BatchDocumentsInputConfig +from .types.document_io import DocumentOutputConfig +from .types.document_io import GcsDocument +from .types.document_io import GcsDocuments +from .types.document_io import GcsPrefix +from .types.document_io import RawDocument from .types.document_processor_service import BatchProcessMetadata from .types.document_processor_service import BatchProcessRequest from .types.document_processor_service import BatchProcessResponse +from .types.document_processor_service import CommonOperationMetadata +from .types.document_processor_service import HumanReviewStatus from .types.document_processor_service import ProcessRequest from .types.document_processor_service import ProcessResponse from .types.document_processor_service import ReviewDocumentOperationMetadata @@ -31,14 +39,22 @@ __all__ = ( + "BatchDocumentsInputConfig", "BatchProcessMetadata", "BatchProcessRequest", "BatchProcessResponse", "BoundingPoly", + "CommonOperationMetadata", "Document", + "DocumentOutputConfig", + "GcsDocument", + "GcsDocuments", + "GcsPrefix", + "HumanReviewStatus", "NormalizedVertex", "ProcessRequest", "ProcessResponse", + "RawDocument", "ReviewDocumentOperationMetadata", "ReviewDocumentRequest", "ReviewDocumentResponse", diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py b/google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py index a2077e22..e6c4f780 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py @@ -95,8 +95,36 @@ class DocumentProcessorServiceAsyncClient: DocumentProcessorServiceClient.parse_common_location_path ) - from_service_account_info = DocumentProcessorServiceClient.from_service_account_info - from_service_account_file = DocumentProcessorServiceClient.from_service_account_file + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + DocumentProcessorServiceAsyncClient: The constructed client. + """ + return DocumentProcessorServiceClient.from_service_account_info.__func__(DocumentProcessorServiceAsyncClient, info, *args, **kwargs) # type: ignore + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + DocumentProcessorServiceAsyncClient: The constructed client. + """ + return DocumentProcessorServiceClient.from_service_account_file.__func__(DocumentProcessorServiceAsyncClient, filename, *args, **kwargs) # type: ignore + from_service_account_json = from_service_account_file @property @@ -225,6 +253,7 @@ async def process_document( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=120.0, ), default_timeout=120.0, client_info=DEFAULT_CLIENT_INFO, @@ -310,6 +339,7 @@ async def batch_process_documents( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=120.0, ), default_timeout=120.0, client_info=DEFAULT_CLIENT_INFO, @@ -404,6 +434,7 @@ async def review_document( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=120.0, ), default_timeout=120.0, client_info=DEFAULT_CLIENT_INFO, diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/base.py b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/base.py index e24d4922..ebcbc6a9 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/base.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/base.py @@ -71,10 +71,10 @@ 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 + 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. @@ -82,6 +82,9 @@ def __init__( host += ":443" self._host = host + # Save the scopes. + self._scopes = scopes or self.AUTH_SCOPES + # If no credentials are provided, then determine the appropriate # defaults. if credentials and credentials_file: @@ -91,20 +94,17 @@ def __init__( if credentials_file is not None: credentials, _ = auth.load_credentials_from_file( - credentials_file, scopes=scopes, quota_project_id=quota_project_id + credentials_file, scopes=self._scopes, quota_project_id=quota_project_id ) elif credentials is None: credentials, _ = auth.default( - scopes=scopes, quota_project_id=quota_project_id + scopes=self._scopes, quota_project_id=quota_project_id ) # Save the credentials. self._credentials = credentials - # Lifted into its own function so it can be stubbed out during tests. - self._prep_wrapped_messages(client_info) - def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. self._wrapped_methods = { @@ -117,6 +117,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=120.0, ), default_timeout=120.0, client_info=client_info, @@ -130,6 +131,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=120.0, ), default_timeout=120.0, client_info=client_info, @@ -143,6 +145,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=120.0, ), default_timeout=120.0, client_info=client_info, diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py index 9c4681da..24a3a7d4 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py @@ -112,7 +112,10 @@ def __init__( google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` and ``credentials_file`` are passed. """ + self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + self._operations_client = None if api_mtls_endpoint: warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) @@ -120,70 +123,50 @@ def __init__( warnings.warn("client_cert_source is deprecated", DeprecationWarning) if channel: - # Sanity check: Ensure that channel and credentials are not both - # provided. + # Ignore credentials if a channel was passed. credentials = False - # If a channel was explicitly provided, set it. self._grpc_channel = channel self._ssl_channel_credentials = None - elif api_mtls_endpoint: - 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: - cert, key = client_cert_source() - ssl_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) - else: - ssl_credentials = SslCredentials().ssl_credentials - # 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_credentials, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - options=[ - ("grpc.max_send_message_length", -1), - ("grpc.max_receive_message_length", -1), - ], - ) - self._ssl_channel_credentials = ssl_credentials else: - host = host if ":" in host else host + ":443" + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials - if credentials is None: - credentials, _ = auth.default( - scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id - ) + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) - if client_cert_source_for_mtls and not ssl_channel_credentials: - cert, key = client_cert_source_for_mtls() - self._ssl_channel_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + ) - # create a new channel. The provided one is ignored. + if not self._grpc_channel: self._grpc_channel = type(self).create_channel( - host, - credentials=credentials, + self._host, + credentials=self._credentials, credentials_file=credentials_file, + scopes=self._scopes, ssl_credentials=self._ssl_channel_credentials, - scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, options=[ ("grpc.max_send_message_length", -1), @@ -191,18 +174,8 @@ def __init__( ], ) - self._stubs = {} # type: Dict[str, Callable] - self._operations_client = None - - # Run the base constructor. - super().__init__( - host=host, - credentials=credentials, - credentials_file=credentials_file, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - client_info=client_info, - ) + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) @classmethod def create_channel( @@ -216,7 +189,7 @@ def create_channel( ) -> grpc.Channel: """Create and return a gRPC channel object. Args: - address (Optional[str]): The host for the channel to use. + host (Optional[str]): The host for the channel to use. credentials (Optional[~.Credentials]): The authorization credentials to attach to requests. These credentials identify this application to the service. If diff --git a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py index 9f46b1c8..99249da3 100644 --- a/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py +++ b/google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py @@ -67,7 +67,7 @@ def create_channel( ) -> aio.Channel: """Create and return a gRPC AsyncIO channel object. Args: - address (Optional[str]): The host for the channel to use. + host (Optional[str]): The host for the channel to use. credentials (Optional[~.Credentials]): The authorization credentials to attach to requests. These credentials identify this application to the service. If @@ -145,10 +145,10 @@ def __init__( ignored if ``channel`` or ``ssl_channel_credentials`` 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 + 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: @@ -157,7 +157,10 @@ def __init__( google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` and ``credentials_file`` are passed. """ + self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + self._operations_client = None if api_mtls_endpoint: warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) @@ -165,70 +168,50 @@ def __init__( warnings.warn("client_cert_source is deprecated", DeprecationWarning) if channel: - # Sanity check: Ensure that channel and credentials are not both - # provided. + # Ignore credentials if a channel was passed. credentials = False - # If a channel was explicitly provided, set it. self._grpc_channel = channel self._ssl_channel_credentials = None - elif api_mtls_endpoint: - 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: - cert, key = client_cert_source() - ssl_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) - else: - ssl_credentials = SslCredentials().ssl_credentials - # 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_credentials, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - options=[ - ("grpc.max_send_message_length", -1), - ("grpc.max_receive_message_length", -1), - ], - ) - self._ssl_channel_credentials = ssl_credentials else: - host = host if ":" in host else host + ":443" + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials - if credentials is None: - credentials, _ = auth.default( - scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id - ) + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) - if client_cert_source_for_mtls and not ssl_channel_credentials: - cert, key = client_cert_source_for_mtls() - self._ssl_channel_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + ) - # create a new channel. The provided one is ignored. + if not self._grpc_channel: self._grpc_channel = type(self).create_channel( - host, - credentials=credentials, + self._host, + credentials=self._credentials, credentials_file=credentials_file, + scopes=self._scopes, ssl_credentials=self._ssl_channel_credentials, - scopes=scopes or self.AUTH_SCOPES, quota_project_id=quota_project_id, options=[ ("grpc.max_send_message_length", -1), @@ -236,18 +219,8 @@ def __init__( ], ) - # Run the base constructor. - super().__init__( - host=host, - credentials=credentials, - credentials_file=credentials_file, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - client_info=client_info, - ) - - self._stubs = {} - self._operations_client = None + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) @property def grpc_channel(self) -> aio.Channel: diff --git a/google/cloud/documentai_v1beta3/types/__init__.py b/google/cloud/documentai_v1beta3/types/__init__.py index 3c34fd3b..0d60bd37 100644 --- a/google/cloud/documentai_v1beta3/types/__init__.py +++ b/google/cloud/documentai_v1beta3/types/__init__.py @@ -15,34 +15,52 @@ # limitations under the License. # -from .geometry import ( - Vertex, - NormalizedVertex, - BoundingPoly, -) from .document import Document +from .document_io import ( + BatchDocumentsInputConfig, + DocumentOutputConfig, + GcsDocument, + GcsDocuments, + GcsPrefix, + RawDocument, +) from .document_processor_service import ( - ProcessRequest, - ProcessResponse, + BatchProcessMetadata, BatchProcessRequest, BatchProcessResponse, - BatchProcessMetadata, + CommonOperationMetadata, + HumanReviewStatus, + ProcessRequest, + ProcessResponse, + ReviewDocumentOperationMetadata, ReviewDocumentRequest, ReviewDocumentResponse, - ReviewDocumentOperationMetadata, +) +from .geometry import ( + BoundingPoly, + NormalizedVertex, + Vertex, ) __all__ = ( - "Vertex", - "NormalizedVertex", - "BoundingPoly", "Document", - "ProcessRequest", - "ProcessResponse", + "BatchDocumentsInputConfig", + "DocumentOutputConfig", + "GcsDocument", + "GcsDocuments", + "GcsPrefix", + "RawDocument", + "BatchProcessMetadata", "BatchProcessRequest", "BatchProcessResponse", - "BatchProcessMetadata", + "CommonOperationMetadata", + "HumanReviewStatus", + "ProcessRequest", + "ProcessResponse", + "ReviewDocumentOperationMetadata", "ReviewDocumentRequest", "ReviewDocumentResponse", - "ReviewDocumentOperationMetadata", + "BoundingPoly", + "NormalizedVertex", + "Vertex", ) diff --git a/google/cloud/documentai_v1beta3/types/document.py b/google/cloud/documentai_v1beta3/types/document.py index f979c519..3290e2dc 100644 --- a/google/cloud/documentai_v1beta3/types/document.py +++ b/google/cloud/documentai_v1beta3/types/document.py @@ -42,24 +42,24 @@ class Document(proto.Message): Attributes: uri (str): - Currently supports Google Cloud Storage URI of the form - ``gs://bucket_name/object_name``. Object versioning is not - supported. See `Google Cloud Storage Request + Optional. Currently supports Google Cloud Storage URI of the + form ``gs://bucket_name/object_name``. Object versioning is + not supported. See `Google Cloud Storage Request URIs `__ for more info. content (bytes): - Inline document content, represented as a stream of bytes. - Note: As with all ``bytes`` fields, protobuffers use a pure - binary representation, whereas JSON representations use - base64. + Optional. Inline document content, represented as a stream + of bytes. Note: As with all ``bytes`` fields, protobuffers + use a pure binary representation, whereas JSON + representations use base64. mime_type (str): An IANA published MIME type (also referred to as media type). For more information, see https://www.iana.org/assignments/media- types/media-types.xhtml. text (str): - UTF-8 encoded text in reading order from the - document. + Optional. UTF-8 encoded text in reading order + from the document. text_styles (Sequence[google.cloud.documentai_v1beta3.types.Document.Style]): Styles for the [Document.text][google.cloud.documentai.v1beta3.Document.text]. @@ -74,11 +74,6 @@ class Document(proto.Message): entity_relations (Sequence[google.cloud.documentai_v1beta3.types.Document.EntityRelation]): Relationship among [Document.entities][google.cloud.documentai.v1beta3.Document.entities]. - translations (Sequence[google.cloud.documentai_v1beta3.types.Document.Translation]): - A list of translations on - [Document.text][google.cloud.documentai.v1beta3.Document.text]. - For document shards, translations in this list may cross - shard boundaries. text_changes (Sequence[google.cloud.documentai_v1beta3.types.Document.TextChange]): A list of text corrections made to [Document.text]. This is usually used for annotating corrections to OCR mistakes. @@ -697,14 +692,16 @@ class Entity(proto.Message): Attributes: text_anchor (google.cloud.documentai_v1beta3.types.Document.TextAnchor): - Provenance of the entity. Text anchor indexing into the + Optional. Provenance of the entity. Text anchor indexing + into the [Document.text][google.cloud.documentai.v1beta3.Document.text]. type_ (str): Entity type from a schema e.g. ``Address``. mention_text (str): - Text value in the document e.g. ``1600 Amphitheatre Pkwy``. + Optional. Text value in the document e.g. + ``1600 Amphitheatre Pkwy``. mention_id (str): - Deprecated. Use ``id`` field instead. + Optional. Deprecated. Use ``id`` field instead. confidence (float): Optional. Confidence of detected Schema entity. Range [0, 1]. @@ -713,8 +710,8 @@ class Entity(proto.Message): entity wrt. the location on the page where it was found. id (str): - Canonical id. This will be a unique value in - the entity list for this document. + Optional. Canonical id. This will be a unique + value in the entity list for this document. normalized_value (google.cloud.documentai_v1beta3.types.Document.Entity.NormalizedValue): Optional. Normalized entity value. Absent if the extracted value could not be converted or @@ -738,23 +735,21 @@ class NormalizedValue(proto.Message): Attributes: money_value (google.type.money_pb2.Money): Money value. See also: - https: - github.com/googleapis/googleapis/blob/master/google/type/money.proto + https://github.com/googleapis/googleapis/blob/master/google/type/money.proto date_value (google.type.date_pb2.Date): Date value. Includes year, month, day. See also: - https: - github.com/googleapis/googleapis/blob/master/google/type/date.proto + https://github.com/googleapis/googleapis/blob/master/google/type/date.proto datetime_value (google.type.datetime_pb2.DateTime): DateTime value. Includes date, time, and timezone. See also: - https: - github.com/googleapis/googleapis/blob/master/google/type/datetime.proto + https://github.com/googleapis/googleapis/blob/master/google/type/datetime.proto address_value (google.type.postal_address_pb2.PostalAddress): Postal address. See also: - - https: - github.com/googleapis/googleapis/blob/master/google/type/postal_address.proto + https://github.com/googleapis/googleapis/blob/master/google/type/postal_address.proto + boolean_value (bool): + Boolean value. Can be used for entities with + binary values, or for checkboxes. text (str): Required. Normalized entity value stored as a string. This field is populated for supported document type (e.g. @@ -791,6 +786,8 @@ class NormalizedValue(proto.Message): message=postal_address.PostalAddress, ) + boolean_value = proto.Field(proto.BOOL, number=6, oneof="structured_value") + text = proto.Field(proto.STRING, number=1) text_anchor = proto.Field( @@ -844,38 +841,6 @@ class EntityRelation(proto.Message): relation = proto.Field(proto.STRING, number=3) - class Translation(proto.Message): - r"""A translation of the text segment. - - Attributes: - text_anchor (google.cloud.documentai_v1beta3.types.Document.TextAnchor): - Provenance of the translation. Text anchor indexing into the - [Document.text][google.cloud.documentai.v1beta3.Document.text]. - There can only be a single ``TextAnchor.text_segments`` - element. If the start and end index of the text segment are - the same, the text change is inserted before that index. - language_code (str): - The BCP-47 language code, such as "en-US" or "sr-Latn". For - more information, see - http://www.unicode.org/reports/tr35/#Unicode_locale_identifier. - translated_text (str): - Text translated into the target language. - provenance (Sequence[google.cloud.documentai_v1beta3.types.Document.Provenance]): - The history of this annotation. - """ - - text_anchor = proto.Field( - proto.MESSAGE, number=1, message="Document.TextAnchor", - ) - - language_code = proto.Field(proto.STRING, number=2) - - translated_text = proto.Field(proto.STRING, number=3) - - provenance = proto.RepeatedField( - proto.MESSAGE, number=4, message="Document.Provenance", - ) - class TextAnchor(proto.Message): r"""Text reference indexing into the [Document.text][google.cloud.documentai.v1beta3.Document.text]. @@ -937,7 +902,8 @@ class PageRef(proto.Message): page (int): Required. Index into the [Document.pages][google.cloud.documentai.v1beta3.Document.pages] - element + element, for example using [Document.pages][page_refs.page] + to locate the related page element. layout_type (google.cloud.documentai_v1beta3.types.Document.PageAnchor.PageRef.LayoutType): Optional. The type of the layout element that is being referenced if any. @@ -1005,6 +971,7 @@ class OperationType(proto.Enum): REPLACE = 3 EVAL_REQUESTED = 4 EVAL_APPROVED = 5 + EVAL_SKIPPED = 6 class Parent(proto.Message): r"""Structure for referencing parent provenances. When an @@ -1134,8 +1101,6 @@ class TextChange(proto.Message): proto.MESSAGE, number=8, message=EntityRelation, ) - translations = proto.RepeatedField(proto.MESSAGE, number=12, message=Translation,) - text_changes = proto.RepeatedField(proto.MESSAGE, number=14, message=TextChange,) shard_info = proto.Field(proto.MESSAGE, number=9, message=ShardInfo,) diff --git a/google/cloud/documentai_v1beta3/types/document_io.py b/google/cloud/documentai_v1beta3/types/document_io.py new file mode 100644 index 00000000..37928c86 --- /dev/null +++ b/google/cloud/documentai_v1beta3/types/document_io.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +# 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 proto # type: ignore + + +__protobuf__ = proto.module( + package="google.cloud.documentai.v1beta3", + manifest={ + "RawDocument", + "GcsDocument", + "GcsDocuments", + "GcsPrefix", + "BatchDocumentsInputConfig", + "DocumentOutputConfig", + }, +) + + +class RawDocument(proto.Message): + r"""Payload message of raw document content (bytes). + + Attributes: + content (bytes): + Inline document content. + mime_type (str): + An IANA MIME type (RFC6838) indicating the nature and format + of the [content]. + """ + + content = proto.Field(proto.BYTES, number=1) + + mime_type = proto.Field(proto.STRING, number=2) + + +class GcsDocument(proto.Message): + r"""Specifies a document stored on Cloud Storage. + + Attributes: + gcs_uri (str): + The Cloud Storage object uri. + mime_type (str): + An IANA MIME type (RFC6838) of the content. + """ + + gcs_uri = proto.Field(proto.STRING, number=1) + + mime_type = proto.Field(proto.STRING, number=2) + + +class GcsDocuments(proto.Message): + r"""Specifies a set of documents on Cloud Storage. + + Attributes: + documents (Sequence[google.cloud.documentai_v1beta3.types.GcsDocument]): + The list of documents. + """ + + documents = proto.RepeatedField(proto.MESSAGE, number=1, message="GcsDocument",) + + +class GcsPrefix(proto.Message): + r"""Specifies all documents on Cloud Storage with a common + prefix. + + Attributes: + gcs_uri_prefix (str): + The URI prefix. + """ + + gcs_uri_prefix = proto.Field(proto.STRING, number=1) + + +class BatchDocumentsInputConfig(proto.Message): + r"""The common config to specify a set of documents used as + input. + + Attributes: + gcs_prefix (google.cloud.documentai_v1beta3.types.GcsPrefix): + The set of documents that match the specified Cloud Storage + [gcs_prefix]. + gcs_documents (google.cloud.documentai_v1beta3.types.GcsDocuments): + The set of documents individually specified + on Cloud Storage. + """ + + gcs_prefix = proto.Field( + proto.MESSAGE, number=1, oneof="source", message="GcsPrefix", + ) + + gcs_documents = proto.Field( + proto.MESSAGE, number=2, oneof="source", message="GcsDocuments", + ) + + +class DocumentOutputConfig(proto.Message): + r"""Config that controls the output of documents. All documents + will be written as a JSON file. + + Attributes: + gcs_output_config (google.cloud.documentai_v1beta3.types.DocumentOutputConfig.GcsOutputConfig): + Output config to write the results to Cloud + Storage. + """ + + class GcsOutputConfig(proto.Message): + r"""The configuration used when outputting documents. + + Attributes: + gcs_uri (str): + The Cloud Storage uri (a directory) of the + output. + """ + + gcs_uri = proto.Field(proto.STRING, number=1) + + gcs_output_config = proto.Field( + proto.MESSAGE, number=1, oneof="destination", message=GcsOutputConfig, + ) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/documentai_v1beta3/types/document_processor_service.py b/google/cloud/documentai_v1beta3/types/document_processor_service.py index 85e71196..3e025e8a 100644 --- a/google/cloud/documentai_v1beta3/types/document_processor_service.py +++ b/google/cloud/documentai_v1beta3/types/document_processor_service.py @@ -19,6 +19,7 @@ from google.cloud.documentai_v1beta3.types import document as gcd_document +from google.cloud.documentai_v1beta3.types import document_io from google.protobuf import timestamp_pb2 as timestamp # type: ignore from google.rpc import status_pb2 as gr_status # type: ignore @@ -27,6 +28,7 @@ package="google.cloud.documentai.v1beta3", manifest={ "ProcessRequest", + "HumanReviewStatus", "ProcessResponse", "BatchProcessRequest", "BatchProcessResponse", @@ -34,6 +36,7 @@ "ReviewDocumentRequest", "ReviewDocumentResponse", "ReviewDocumentOperationMetadata", + "CommonOperationMetadata", }, ) @@ -42,6 +45,10 @@ class ProcessRequest(proto.Message): r"""Request message for the process document method. Attributes: + inline_document (google.cloud.documentai_v1beta3.types.Document): + An inline document proto. + raw_document (google.cloud.documentai_v1beta3.types.RawDocument): + A raw document content (bytes). name (str): Required. The processor resource name. document (google.cloud.documentai_v1beta3.types.Document): @@ -52,6 +59,14 @@ class ProcessRequest(proto.Message): skipped for this request. Default to false. """ + inline_document = proto.Field( + proto.MESSAGE, number=4, oneof="source", message=gcd_document.Document, + ) + + raw_document = proto.Field( + proto.MESSAGE, number=5, oneof="source", message=document_io.RawDocument, + ) + name = proto.Field(proto.STRING, number=1) document = proto.Field(proto.MESSAGE, number=2, message=gcd_document.Document,) @@ -59,6 +74,39 @@ class ProcessRequest(proto.Message): skip_human_review = proto.Field(proto.BOOL, number=3) +class HumanReviewStatus(proto.Message): + r"""The status of human review on a processed document. + + Attributes: + state (google.cloud.documentai_v1beta3.types.HumanReviewStatus.State): + The state of human review on the processing + request. + state_message (str): + A message providing more details about the + human review state. + human_review_operation (str): + The name of the operation triggered by the processed + document. This field is populated only when the [state] is + [HUMAN_REVIEW_IN_PROGRESS]. It has the same response type + and metadata as the long running operation returned by + [ReviewDocument] method. + """ + + class State(proto.Enum): + r"""The final state of human review on a processed document.""" + STATE_UNSPECIFIED = 0 + SKIPPED = 1 + VALIDATION_PASSED = 2 + IN_PROGRESS = 3 + ERROR = 4 + + state = proto.Field(proto.ENUM, number=1, enum=State,) + + state_message = proto.Field(proto.STRING, number=2) + + human_review_operation = proto.Field(proto.STRING, number=3) + + class ProcessResponse(proto.Message): r"""Response message for the process document method. @@ -73,12 +121,19 @@ class ProcessResponse(proto.Message): has the same response type and metadata as the long running operation returned by ReviewDocument method. + human_review_status (google.cloud.documentai_v1beta3.types.HumanReviewStatus): + The status of human review on the processed + document. """ document = proto.Field(proto.MESSAGE, number=1, message=gcd_document.Document,) human_review_operation = proto.Field(proto.STRING, number=2) + human_review_status = proto.Field( + proto.MESSAGE, number=3, message="HumanReviewStatus", + ) + class BatchProcessRequest(proto.Message): r"""Request message for batch process document method. @@ -91,6 +146,13 @@ class BatchProcessRequest(proto.Message): the batch process. output_config (google.cloud.documentai_v1beta3.types.BatchProcessRequest.BatchOutputConfig): The overall output config for batch process. + input_documents (google.cloud.documentai_v1beta3.types.BatchDocumentsInputConfig): + The input documents for batch process. + document_output_config (google.cloud.documentai_v1beta3.types.DocumentOutputConfig): + The overall output config for batch process. + skip_human_review (bool): + Whether Human Review feature should be + skipped for this request. Default to false. """ class BatchInputConfig(proto.Message): @@ -130,6 +192,16 @@ class BatchOutputConfig(proto.Message): output_config = proto.Field(proto.MESSAGE, number=3, message=BatchOutputConfig,) + input_documents = proto.Field( + proto.MESSAGE, number=5, message=document_io.BatchDocumentsInputConfig, + ) + + document_output_config = proto.Field( + proto.MESSAGE, number=6, message=document_io.DocumentOutputConfig, + ) + + skip_human_review = proto.Field(proto.BOOL, number=4) + class BatchProcessResponse(proto.Message): r"""Response message for batch process document method.""" @@ -188,6 +260,9 @@ class IndividualProcessStatus(proto.Message): has the same response type and metadata as the long running operation returned by ReviewDocument method. + human_review_status (google.cloud.documentai_v1beta3.types.HumanReviewStatus): + The status of human review on the processed + document. """ input_gcs_source = proto.Field(proto.STRING, number=1) @@ -198,6 +273,10 @@ class IndividualProcessStatus(proto.Message): human_review_operation = proto.Field(proto.STRING, number=4) + human_review_status = proto.Field( + proto.MESSAGE, number=5, message="HumanReviewStatus", + ) + state = proto.Field(proto.ENUM, number=1, enum=State,) state_message = proto.Field(proto.STRING, number=2) @@ -215,6 +294,8 @@ class ReviewDocumentRequest(proto.Message): r"""Request message for review document method. Attributes: + inline_document (google.cloud.documentai_v1beta3.types.Document): + An inline document proto. human_review_config (str): Required. The resource name of the HumanReviewConfig that the document will be @@ -223,6 +304,10 @@ class ReviewDocumentRequest(proto.Message): The document that needs human review. """ + inline_document = proto.Field( + proto.MESSAGE, number=4, oneof="source", message=gcd_document.Document, + ) + human_review_config = proto.Field(proto.STRING, number=1) document = proto.Field(proto.MESSAGE, number=2, message=gcd_document.Document,) @@ -255,6 +340,46 @@ class ReviewDocumentOperationMetadata(proto.Message): The creation time of the operation. update_time (google.protobuf.timestamp_pb2.Timestamp): The last update time of the operation. + common_metadata (google.cloud.documentai_v1beta3.types.CommonOperationMetadata): + The basic metadata of the long running + operation. + """ + + class State(proto.Enum): + r"""State of the longrunning operation.""" + STATE_UNSPECIFIED = 0 + RUNNING = 1 + CANCELLING = 2 + SUCCEEDED = 3 + FAILED = 4 + CANCELLED = 5 + + state = proto.Field(proto.ENUM, number=1, enum=State,) + + state_message = proto.Field(proto.STRING, number=2) + + create_time = proto.Field(proto.MESSAGE, number=3, message=timestamp.Timestamp,) + + update_time = proto.Field(proto.MESSAGE, number=4, message=timestamp.Timestamp,) + + common_metadata = proto.Field( + proto.MESSAGE, number=5, message="CommonOperationMetadata", + ) + + +class CommonOperationMetadata(proto.Message): + r"""The common metadata for long running operations. + + Attributes: + state (google.cloud.documentai_v1beta3.types.CommonOperationMetadata.State): + The state of the operation. + state_message (str): + A message providing more details about the + current state of processing. + create_time (google.protobuf.timestamp_pb2.Timestamp): + The creation time of the operation. + update_time (google.protobuf.timestamp_pb2.Timestamp): + The last update time of the operation. """ class State(proto.Enum): diff --git a/noxfile.py b/noxfile.py index 8d9724d0..ae8392be 100644 --- a/noxfile.py +++ b/noxfile.py @@ -18,6 +18,7 @@ from __future__ import absolute_import import os +import pathlib import shutil import nox @@ -30,6 +31,8 @@ SYSTEM_TEST_PYTHON_VERSIONS = ["3.8"] UNIT_TEST_PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9"] +CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() + # 'docfx' is excluded since it only needs to run in 'docs-presubmit' nox.options.sessions = [ "unit", @@ -41,6 +44,9 @@ "docs", ] +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + @nox.session(python=DEFAULT_PYTHON_VERSION) def lint(session): @@ -81,13 +87,15 @@ def lint_setup_py(session): def default(session): # Install all test dependencies, then install this package in-place. - session.install("asyncmock", "pytest-asyncio") - session.install( - "mock", "pytest", "pytest-cov", + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" ) + session.install("asyncmock", "pytest-asyncio", "-c", constraints_path) - session.install("-e", ".") + session.install("mock", "pytest", "pytest-cov", "-c", constraints_path) + + session.install("-e", ".", "-c", constraints_path) # Run py.test against the unit tests. session.run( @@ -114,6 +122,9 @@ def unit(session): @nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) def system(session): """Run the system test suite.""" + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + ) system_test_path = os.path.join("tests", "system.py") system_test_folder_path = os.path.join("tests", "system") @@ -138,10 +149,8 @@ def system(session): # Install all test dependencies, then install this package into the # virtualenv's dist-packages. - session.install( - "mock", "pytest", "google-cloud-testutils", - ) - session.install("-e", ".") + session.install("mock", "pytest", "google-cloud-testutils", "-c", constraints_path) + session.install("-e", ".", "-c", constraints_path) # Run py.test against the system tests. if system_test_exists: @@ -170,7 +179,7 @@ def cover(session): test runs (not system test runs), and then erases coverage data. """ session.install("coverage", "pytest-cov") - session.run("coverage", "report", "--show-missing", "--fail-under=100") + session.run("coverage", "report", "--show-missing", "--fail-under=99") session.run("coverage", "erase") diff --git a/renovate.json b/renovate.json index 4fa94931..f08bc22c 100644 --- a/renovate.json +++ b/renovate.json @@ -1,5 +1,6 @@ { "extends": [ "config:base", ":preserveSemverRanges" - ] + ], + "ignorePaths": [".pre-commit-config.yaml"] } diff --git a/scripts/fixup_documentai_v1beta2_keywords.py b/scripts/fixup_documentai_v1beta2_keywords.py deleted file mode 100644 index d2f24146..00000000 --- a/scripts/fixup_documentai_v1beta2_keywords.py +++ /dev/null @@ -1,180 +0,0 @@ -#! /usr/bin/env python3 -# -*- coding: utf-8 -*- - -# 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 argparse -import os -import libcst as cst -import pathlib -import sys -from typing import (Any, Callable, Dict, List, Sequence, Tuple) - - -def partition( - predicate: Callable[[Any], bool], - iterator: Sequence[Any] -) -> Tuple[List[Any], List[Any]]: - """A stable, out-of-place partition.""" - results = ([], []) - - for i in iterator: - results[int(predicate(i))].append(i) - - # Returns trueList, falseList - return results[1], results[0] - - -class documentaiCallTransformer(cst.CSTTransformer): - CTRL_PARAMS: Tuple[str] = ('retry', 'timeout', 'metadata') - METHOD_TO_PARAMS: Dict[str, Tuple[str]] = { - 'batch_process_documents': ('requests', 'parent', ), - 'process_document': ('input_config', 'parent', 'output_config', 'document_type', 'table_extraction_params', 'form_extraction_params', 'entity_extraction_params', 'ocr_params', 'automl_params', ), - - } - - def leave_Call(self, original: cst.Call, updated: cst.Call) -> cst.CSTNode: - try: - key = original.func.attr.value - kword_params = self.METHOD_TO_PARAMS[key] - except (AttributeError, KeyError): - # Either not a method from the API or too convoluted to be sure. - return updated - - # If the existing code is valid, keyword args come after positional args. - # Therefore, all positional args must map to the first parameters. - args, kwargs = partition(lambda a: not bool(a.keyword), updated.args) - if any(k.keyword.value == "request" for k in kwargs): - # We've already fixed this file, don't fix it again. - return updated - - kwargs, ctrl_kwargs = partition( - lambda a: not a.keyword.value in self.CTRL_PARAMS, - kwargs - ) - - args, ctrl_args = args[:len(kword_params)], args[len(kword_params):] - ctrl_kwargs.extend(cst.Arg(value=a.value, keyword=cst.Name(value=ctrl)) - for a, ctrl in zip(ctrl_args, self.CTRL_PARAMS)) - - request_arg = cst.Arg( - value=cst.Dict([ - cst.DictElement( - cst.SimpleString("'{}'".format(name)), - cst.Element(value=arg.value) - ) - # Note: the args + kwargs looks silly, but keep in mind that - # the control parameters had to be stripped out, and that - # those could have been passed positionally or by keyword. - for name, arg in zip(kword_params, args + kwargs)]), - keyword=cst.Name("request") - ) - - return updated.with_changes( - args=[request_arg] + ctrl_kwargs - ) - - -def fix_files( - in_dir: pathlib.Path, - out_dir: pathlib.Path, - *, - transformer=documentaiCallTransformer(), -): - """Duplicate the input dir to the output dir, fixing file method calls. - - Preconditions: - * in_dir is a real directory - * out_dir is a real, empty directory - """ - pyfile_gen = ( - pathlib.Path(os.path.join(root, f)) - for root, _, files in os.walk(in_dir) - for f in files if os.path.splitext(f)[1] == ".py" - ) - - for fpath in pyfile_gen: - with open(fpath, 'r') as f: - src = f.read() - - # Parse the code and insert method call fixes. - tree = cst.parse_module(src) - updated = tree.visit(transformer) - - # Create the path and directory structure for the new file. - updated_path = out_dir.joinpath(fpath.relative_to(in_dir)) - updated_path.parent.mkdir(parents=True, exist_ok=True) - - # Generate the updated source file at the corresponding path. - with open(updated_path, 'w') as f: - f.write(updated.code) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description="""Fix up source that uses the documentai client library. - -The existing sources are NOT overwritten but are copied to output_dir with changes made. - -Note: This tool operates at a best-effort level at converting positional - parameters in client method calls to keyword based parameters. - Cases where it WILL FAIL include - A) * or ** expansion in a method call. - B) Calls via function or method alias (includes free function calls) - C) Indirect or dispatched calls (e.g. the method is looked up dynamically) - - These all constitute false negatives. The tool will also detect false - positives when an API method shares a name with another method. -""") - parser.add_argument( - '-d', - '--input-directory', - required=True, - dest='input_dir', - help='the input directory to walk for python files to fix up', - ) - parser.add_argument( - '-o', - '--output-directory', - required=True, - dest='output_dir', - help='the directory to output files fixed via un-flattening', - ) - args = parser.parse_args() - input_dir = pathlib.Path(args.input_dir) - output_dir = pathlib.Path(args.output_dir) - if not input_dir.is_dir(): - print( - f"input directory '{input_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if not output_dir.is_dir(): - print( - f"output directory '{output_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if os.listdir(output_dir): - print( - f"output directory '{output_dir}' is not empty", - file=sys.stderr, - ) - sys.exit(-1) - - fix_files(input_dir, output_dir) diff --git a/scripts/fixup_documentai_v1beta3_keywords.py b/scripts/fixup_documentai_v1beta3_keywords.py deleted file mode 100644 index 750630f1..00000000 --- a/scripts/fixup_documentai_v1beta3_keywords.py +++ /dev/null @@ -1,181 +0,0 @@ -#! /usr/bin/env python3 -# -*- coding: utf-8 -*- - -# 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 argparse -import os -import libcst as cst -import pathlib -import sys -from typing import (Any, Callable, Dict, List, Sequence, Tuple) - - -def partition( - predicate: Callable[[Any], bool], - iterator: Sequence[Any] -) -> Tuple[List[Any], List[Any]]: - """A stable, out-of-place partition.""" - results = ([], []) - - for i in iterator: - results[int(predicate(i))].append(i) - - # Returns trueList, falseList - return results[1], results[0] - - -class documentaiCallTransformer(cst.CSTTransformer): - CTRL_PARAMS: Tuple[str] = ('retry', 'timeout', 'metadata') - METHOD_TO_PARAMS: Dict[str, Tuple[str]] = { - 'batch_process_documents': ('name', 'input_configs', 'output_config', ), - 'process_document': ('name', 'document', 'skip_human_review', ), - 'review_document': ('human_review_config', 'document', ), - - } - - def leave_Call(self, original: cst.Call, updated: cst.Call) -> cst.CSTNode: - try: - key = original.func.attr.value - kword_params = self.METHOD_TO_PARAMS[key] - except (AttributeError, KeyError): - # Either not a method from the API or too convoluted to be sure. - return updated - - # If the existing code is valid, keyword args come after positional args. - # Therefore, all positional args must map to the first parameters. - args, kwargs = partition(lambda a: not bool(a.keyword), updated.args) - if any(k.keyword.value == "request" for k in kwargs): - # We've already fixed this file, don't fix it again. - return updated - - kwargs, ctrl_kwargs = partition( - lambda a: not a.keyword.value in self.CTRL_PARAMS, - kwargs - ) - - args, ctrl_args = args[:len(kword_params)], args[len(kword_params):] - ctrl_kwargs.extend(cst.Arg(value=a.value, keyword=cst.Name(value=ctrl)) - for a, ctrl in zip(ctrl_args, self.CTRL_PARAMS)) - - request_arg = cst.Arg( - value=cst.Dict([ - cst.DictElement( - cst.SimpleString("'{}'".format(name)), - cst.Element(value=arg.value) - ) - # Note: the args + kwargs looks silly, but keep in mind that - # the control parameters had to be stripped out, and that - # those could have been passed positionally or by keyword. - for name, arg in zip(kword_params, args + kwargs)]), - keyword=cst.Name("request") - ) - - return updated.with_changes( - args=[request_arg] + ctrl_kwargs - ) - - -def fix_files( - in_dir: pathlib.Path, - out_dir: pathlib.Path, - *, - transformer=documentaiCallTransformer(), -): - """Duplicate the input dir to the output dir, fixing file method calls. - - Preconditions: - * in_dir is a real directory - * out_dir is a real, empty directory - """ - pyfile_gen = ( - pathlib.Path(os.path.join(root, f)) - for root, _, files in os.walk(in_dir) - for f in files if os.path.splitext(f)[1] == ".py" - ) - - for fpath in pyfile_gen: - with open(fpath, 'r') as f: - src = f.read() - - # Parse the code and insert method call fixes. - tree = cst.parse_module(src) - updated = tree.visit(transformer) - - # Create the path and directory structure for the new file. - updated_path = out_dir.joinpath(fpath.relative_to(in_dir)) - updated_path.parent.mkdir(parents=True, exist_ok=True) - - # Generate the updated source file at the corresponding path. - with open(updated_path, 'w') as f: - f.write(updated.code) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description="""Fix up source that uses the documentai client library. - -The existing sources are NOT overwritten but are copied to output_dir with changes made. - -Note: This tool operates at a best-effort level at converting positional - parameters in client method calls to keyword based parameters. - Cases where it WILL FAIL include - A) * or ** expansion in a method call. - B) Calls via function or method alias (includes free function calls) - C) Indirect or dispatched calls (e.g. the method is looked up dynamically) - - These all constitute false negatives. The tool will also detect false - positives when an API method shares a name with another method. -""") - parser.add_argument( - '-d', - '--input-directory', - required=True, - dest='input_dir', - help='the input directory to walk for python files to fix up', - ) - parser.add_argument( - '-o', - '--output-directory', - required=True, - dest='output_dir', - help='the directory to output files fixed via un-flattening', - ) - args = parser.parse_args() - input_dir = pathlib.Path(args.input_dir) - output_dir = pathlib.Path(args.output_dir) - if not input_dir.is_dir(): - print( - f"input directory '{input_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if not output_dir.is_dir(): - print( - f"output directory '{output_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if os.listdir(output_dir): - print( - f"output directory '{output_dir}' is not empty", - file=sys.stderr, - ) - sys.exit(-1) - - fix_files(input_dir, output_dir) diff --git a/scripts/fixup_keywords.py b/scripts/fixup_keywords.py deleted file mode 100644 index 18b62b1c..00000000 --- a/scripts/fixup_keywords.py +++ /dev/null @@ -1,178 +0,0 @@ -# -*- coding: utf-8 -*- - -# 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 argparse -import os -import libcst as cst -import pathlib -import sys -from typing import (Any, Callable, Dict, List, Sequence, Tuple) - - -def partition( - predicate: Callable[[Any], bool], - iterator: Sequence[Any] -) -> Tuple[List[Any], List[Any]]: - """A stable, out-of-place partition.""" - results = ([], []) - - for i in iterator: - results[int(predicate(i))].append(i) - - # Returns trueList, falseList - return results[1], results[0] - - -class documentaiCallTransformer(cst.CSTTransformer): - CTRL_PARAMS: Tuple[str] = ('retry', 'timeout', 'metadata') - METHOD_TO_PARAMS: Dict[str, Tuple[str]] = { - 'batch_process_documents': ('requests', 'parent', ), - 'process_document': ('input_config', 'parent', 'output_config', 'document_type', 'table_extraction_params', 'form_extraction_params', 'entity_extraction_params', 'ocr_params', 'automl_params', ), - } - - def leave_Call(self, original: cst.Call, updated: cst.Call) -> cst.CSTNode: - try: - key = original.func.attr.value - kword_params = self.METHOD_TO_PARAMS[key] - except (AttributeError, KeyError): - # Either not a method from the API or too convoluted to be sure. - return updated - - # If the existing code is valid, keyword args come after positional args. - # Therefore, all positional args must map to the first parameters. - args, kwargs = partition(lambda a: not bool(a.keyword), updated.args) - if any(k.keyword.value == "request" for k in kwargs): - # We've already fixed this file, don't fix it again. - return updated - - kwargs, ctrl_kwargs = partition( - lambda a: not a.keyword.value in self.CTRL_PARAMS, - kwargs - ) - - args, ctrl_args = args[:len(kword_params)], args[len(kword_params):] - ctrl_kwargs.extend(cst.Arg(value=a.value, keyword=cst.Name(value=ctrl)) - for a, ctrl in zip(ctrl_args, self.CTRL_PARAMS)) - - request_arg = cst.Arg( - value=cst.Dict([ - cst.DictElement( - cst.SimpleString("'{}'".format(name)), - cst.Element(value=arg.value) - ) - # Note: the args + kwargs looks silly, but keep in mind that - # the control parameters had to be stripped out, and that - # those could have been passed positionally or by keyword. - for name, arg in zip(kword_params, args + kwargs)]), - keyword=cst.Name("request") - ) - - return updated.with_changes( - args=[request_arg] + ctrl_kwargs - ) - - -def fix_files( - in_dir: pathlib.Path, - out_dir: pathlib.Path, - *, - transformer=documentaiCallTransformer(), -): - """Duplicate the input dir to the output dir, fixing file method calls. - - Preconditions: - * in_dir is a real directory - * out_dir is a real, empty directory - """ - pyfile_gen = ( - pathlib.Path(os.path.join(root, f)) - for root, _, files in os.walk(in_dir) - for f in files if os.path.splitext(f)[1] == ".py" - ) - - for fpath in pyfile_gen: - with open(fpath, 'r') as f: - src = f.read() - - # Parse the code and insert method call fixes. - tree = cst.parse_module(src) - updated = tree.visit(transformer) - - # Create the path and directory structure for the new file. - updated_path = out_dir.joinpath(fpath.relative_to(in_dir)) - updated_path.parent.mkdir(parents=True, exist_ok=True) - - # Generate the updated source file at the corresponding path. - with open(updated_path, 'w') as f: - f.write(updated.code) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description="""Fix up source that uses the documentai client library. - -The existing sources are NOT overwritten but are copied to output_dir with changes made. - -Note: This tool operates at a best-effort level at converting positional - parameters in client method calls to keyword based parameters. - Cases where it WILL FAIL include - A) * or ** expansion in a method call. - B) Calls via function or method alias (includes free function calls) - C) Indirect or dispatched calls (e.g. the method is looked up dynamically) - - These all constitute false negatives. The tool will also detect false - positives when an API method shares a name with another method. -""") - parser.add_argument( - '-d', - '--input-directory', - required=True, - dest='input_dir', - help='the input directory to walk for python files to fix up', - ) - parser.add_argument( - '-o', - '--output-directory', - required=True, - dest='output_dir', - help='the directory to output files fixed via un-flattening', - ) - args = parser.parse_args() - input_dir = pathlib.Path(args.input_dir) - output_dir = pathlib.Path(args.output_dir) - if not input_dir.is_dir(): - print( - f"input directory '{input_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if not output_dir.is_dir(): - print( - f"output directory '{output_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if os.listdir(output_dir): - print( - f"output directory '{output_dir}' is not empty", - file=sys.stderr, - ) - sys.exit(-1) - - fix_files(input_dir, output_dir) diff --git a/setup.py b/setup.py index 90005d59..fac3d0ca 100644 --- a/setup.py +++ b/setup.py @@ -41,12 +41,11 @@ platforms="Posix; MacOS X; Windows", include_package_data=True, install_requires=( - "google-api-core[grpc] >= 1.22.0, < 2.0.0dev", + "google-api-core[grpc] >= 1.22.2, < 2.0.0dev", "proto-plus >= 1.10.0", ), python_requires=">=3.6", setup_requires=["libcst >= 0.2.5"], - scripts=["scripts/fixup_keywords.py"], classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", diff --git a/synth.metadata b/synth.metadata index ed91bb7e..1d63eab3 100644 --- a/synth.metadata +++ b/synth.metadata @@ -3,30 +3,30 @@ { "git": { "name": ".", - "remote": "https://github.com/googleapis/python-documentai.git", - "sha": "b78db75d0f3c68a38fbc5e540d264f11f5abfd3b" + "remote": "git@github.com:googleapis/python-documentai", + "sha": "9fd02a6b9ba34a6762a762f12de9948daf1ea9bb" } }, { "git": { "name": "googleapis", "remote": "https://github.com/googleapis/googleapis.git", - "sha": "20712b8fe95001b312f62c6c5f33e3e3ec92cfaf", - "internalRef": "354996675" + "sha": "551ddbb55b96147012c00b66250dd5907556807c", + "internalRef": "364734171" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "0780323da96d5a53925fe0547757181fe76e8f1e" + "sha": "7a3df8832c7c64c482874c5dbebfd0a732b4938b" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "0780323da96d5a53925fe0547757181fe76e8f1e" + "sha": "7a3df8832c7c64c482874c5dbebfd0a732b4938b" } } ], @@ -48,120 +48,15 @@ "language": "python", "generator": "bazel" } + }, + { + "client": { + "source": "googleapis", + "apiName": "documentai", + "apiVersion": "v1", + "language": "python", + "generator": "bazel" + } } - ], - "generatedFiles": [ - ".coveragerc", - ".flake8", - ".github/CONTRIBUTING.md", - ".github/ISSUE_TEMPLATE/bug_report.md", - ".github/ISSUE_TEMPLATE/feature_request.md", - ".github/ISSUE_TEMPLATE/support_request.md", - ".github/PULL_REQUEST_TEMPLATE.md", - ".github/header-checker-lint.yml", - ".github/release-please.yml", - ".github/snippet-bot.yml", - ".gitignore", - ".kokoro/build.sh", - ".kokoro/continuous/common.cfg", - ".kokoro/continuous/continuous.cfg", - ".kokoro/docker/docs/Dockerfile", - ".kokoro/docker/docs/fetch_gpg_keys.sh", - ".kokoro/docs/common.cfg", - ".kokoro/docs/docs-presubmit.cfg", - ".kokoro/docs/docs.cfg", - ".kokoro/populate-secrets.sh", - ".kokoro/presubmit/common.cfg", - ".kokoro/presubmit/presubmit.cfg", - ".kokoro/publish-docs.sh", - ".kokoro/release.sh", - ".kokoro/release/common.cfg", - ".kokoro/release/release.cfg", - ".kokoro/samples/lint/common.cfg", - ".kokoro/samples/lint/continuous.cfg", - ".kokoro/samples/lint/periodic.cfg", - ".kokoro/samples/lint/presubmit.cfg", - ".kokoro/samples/python3.6/common.cfg", - ".kokoro/samples/python3.6/continuous.cfg", - ".kokoro/samples/python3.6/periodic.cfg", - ".kokoro/samples/python3.6/presubmit.cfg", - ".kokoro/samples/python3.7/common.cfg", - ".kokoro/samples/python3.7/continuous.cfg", - ".kokoro/samples/python3.7/periodic.cfg", - ".kokoro/samples/python3.7/presubmit.cfg", - ".kokoro/samples/python3.8/common.cfg", - ".kokoro/samples/python3.8/continuous.cfg", - ".kokoro/samples/python3.8/periodic.cfg", - ".kokoro/samples/python3.8/presubmit.cfg", - ".kokoro/test-samples.sh", - ".kokoro/trampoline.sh", - ".kokoro/trampoline_v2.sh", - ".pre-commit-config.yaml", - ".trampolinerc", - "CODE_OF_CONDUCT.md", - "CONTRIBUTING.rst", - "LICENSE", - "MANIFEST.in", - "docs/_static/custom.css", - "docs/_templates/layout.html", - "docs/conf.py", - "docs/documentai_v1beta2/document_understanding_service.rst", - "docs/documentai_v1beta2/services.rst", - "docs/documentai_v1beta2/types.rst", - "docs/documentai_v1beta3/document_processor_service.rst", - "docs/documentai_v1beta3/services.rst", - "docs/documentai_v1beta3/types.rst", - "docs/multiprocessing.rst", - "google/cloud/documentai/__init__.py", - "google/cloud/documentai/py.typed", - "google/cloud/documentai_v1beta2/__init__.py", - "google/cloud/documentai_v1beta2/py.typed", - "google/cloud/documentai_v1beta2/services/__init__.py", - "google/cloud/documentai_v1beta2/services/document_understanding_service/__init__.py", - "google/cloud/documentai_v1beta2/services/document_understanding_service/async_client.py", - "google/cloud/documentai_v1beta2/services/document_understanding_service/client.py", - "google/cloud/documentai_v1beta2/services/document_understanding_service/transports/__init__.py", - "google/cloud/documentai_v1beta2/services/document_understanding_service/transports/base.py", - "google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc.py", - "google/cloud/documentai_v1beta2/services/document_understanding_service/transports/grpc_asyncio.py", - "google/cloud/documentai_v1beta2/types/__init__.py", - "google/cloud/documentai_v1beta2/types/document.py", - "google/cloud/documentai_v1beta2/types/document_understanding.py", - "google/cloud/documentai_v1beta2/types/geometry.py", - "google/cloud/documentai_v1beta3/__init__.py", - "google/cloud/documentai_v1beta3/py.typed", - "google/cloud/documentai_v1beta3/services/__init__.py", - "google/cloud/documentai_v1beta3/services/document_processor_service/__init__.py", - "google/cloud/documentai_v1beta3/services/document_processor_service/async_client.py", - "google/cloud/documentai_v1beta3/services/document_processor_service/client.py", - "google/cloud/documentai_v1beta3/services/document_processor_service/transports/__init__.py", - "google/cloud/documentai_v1beta3/services/document_processor_service/transports/base.py", - "google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc.py", - "google/cloud/documentai_v1beta3/services/document_processor_service/transports/grpc_asyncio.py", - "google/cloud/documentai_v1beta3/types/__init__.py", - "google/cloud/documentai_v1beta3/types/document.py", - "google/cloud/documentai_v1beta3/types/document_processor_service.py", - "google/cloud/documentai_v1beta3/types/geometry.py", - "mypy.ini", - "noxfile.py", - "renovate.json", - "samples/AUTHORING_GUIDE.md", - "samples/CONTRIBUTING.md", - "samples/snippets/noxfile.py", - "scripts/decrypt-secrets.sh", - "scripts/fixup_documentai_v1beta2_keywords.py", - "scripts/fixup_documentai_v1beta3_keywords.py", - "scripts/readme-gen/readme_gen.py", - "scripts/readme-gen/templates/README.tmpl.rst", - "scripts/readme-gen/templates/auth.tmpl.rst", - "scripts/readme-gen/templates/auth_api_key.tmpl.rst", - "scripts/readme-gen/templates/install_deps.tmpl.rst", - "scripts/readme-gen/templates/install_portaudio.tmpl.rst", - "setup.cfg", - "testing/.gitignore", - "tests/unit/gapic/documentai_v1beta2/__init__.py", - "tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py", - "tests/unit/gapic/documentai_v1beta3/__init__.py", - "tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py" ] } \ No newline at end of file diff --git a/synth.py b/synth.py index 3281f52f..34c054d6 100644 --- a/synth.py +++ b/synth.py @@ -25,12 +25,11 @@ gapic = gcp.GAPICBazel() common = gcp.CommonTemplates() +# add the highest stable version to the end +versions = ["v1beta2", "v1beta3", "v1"] # ---------------------------------------------------------------------------- # Generate document AI GAPIC layer # ---------------------------------------------------------------------------- - -versions = ["v1beta2", "v1beta3"] - for version in versions: library = gapic.py_library( service="documentai", @@ -38,21 +37,26 @@ bazel_target=f"//google/cloud/documentai/{version}:documentai-{version}-py", ) - excludes = ["README.rst", "nox.py", "docs/index.rst", "setup.py"] + excludes = [ + "README.rst", + "nox.py", + "docs/index.rst", + "setup.py", + "scripts/fixup_documentai_v*", # this library was always generated with the microgenerator + ] s.move(library, excludes=excludes) # ---------------------------------------------------------------------------- # Add templated files # ---------------------------------------------------------------------------- templated_files = common.py_library( - cov_level=100, - microgenerator=True, - samples=False, # set to true if there are samples + cov_level=99, microgenerator=True, samples=False, # set to true if there are samples ) + s.move( templated_files, excludes=[".coveragerc"], # microgenerator has a good .coveragerc file -) +) python.py_samples(skip_readmes=True) diff --git a/testing/constraints-3.10.txt b/testing/constraints-3.10.txt new file mode 100644 index 00000000..e69de29b diff --git a/testing/constraints-3.11.txt b/testing/constraints-3.11.txt new file mode 100644 index 00000000..e69de29b diff --git a/testing/constraints-3.6.txt b/testing/constraints-3.6.txt new file mode 100644 index 00000000..69e1c139 --- /dev/null +++ b/testing/constraints-3.6.txt @@ -0,0 +1,9 @@ +# This constraints file is used to check that lower bounds +# are correct in setup.py +# List *all* library dependencies and extras in this file. +# Pin the version to the lower bound. +# +# e.g., if setup.py has "foo >= 1.14.0, < 2.0.0dev", +# Then this file should have foo==1.14.0 +google-api-core==1.22.2 +proto-plus==1.10.0 diff --git a/testing/constraints-3.7.txt b/testing/constraints-3.7.txt new file mode 100644 index 00000000..e69de29b diff --git a/testing/constraints-3.8.txt b/testing/constraints-3.8.txt new file mode 100644 index 00000000..e69de29b diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/gapic/documentai_v1/__init__.py b/tests/unit/gapic/documentai_v1/__init__.py new file mode 100644 index 00000000..42ffdf2b --- /dev/null +++ b/tests/unit/gapic/documentai_v1/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +# 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. +# diff --git a/tests/unit/gapic/documentai_v1/test_document_processor_service.py b/tests/unit/gapic/documentai_v1/test_document_processor_service.py new file mode 100644 index 00000000..b48bdc60 --- /dev/null +++ b/tests/unit/gapic/documentai_v1/test_document_processor_service.py @@ -0,0 +1,1723 @@ +# -*- coding: utf-8 -*- + +# 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 mock + +import grpc +from grpc.experimental import aio +import math +import pytest +from proto.marshal.rules.dates import DurationRule, TimestampRule + +from google import auth +from google.api_core import client_options +from google.api_core import exceptions +from google.api_core import future +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 # type: ignore +from google.api_core import operations_v1 +from google.auth import credentials +from google.auth.exceptions import MutualTLSChannelError +from google.cloud.documentai_v1.services.document_processor_service import ( + DocumentProcessorServiceAsyncClient, +) +from google.cloud.documentai_v1.services.document_processor_service import ( + DocumentProcessorServiceClient, +) +from google.cloud.documentai_v1.services.document_processor_service import transports +from google.cloud.documentai_v1.types import document +from google.cloud.documentai_v1.types import document_io +from google.cloud.documentai_v1.types import document_processor_service +from google.cloud.documentai_v1.types import geometry +from google.longrunning import operations_pb2 +from google.oauth2 import service_account +from google.protobuf import any_pb2 as gp_any # type: ignore +from google.protobuf import duration_pb2 as duration # type: ignore +from google.protobuf import timestamp_pb2 as timestamp # type: ignore +from google.protobuf import wrappers_pb2 as wrappers # type: ignore +from google.rpc import status_pb2 as status # type: ignore +from google.type import color_pb2 as color # type: ignore +from google.type import date_pb2 as date # type: ignore +from google.type import datetime_pb2 as datetime # type: ignore +from google.type import money_pb2 as money # type: ignore +from google.type import postal_address_pb2 as postal_address # type: ignore + + +def client_cert_source_callback(): + return b"cert bytes", b"key bytes" + + +# If default endpoint is localhost, then default mtls endpoint will be the same. +# This method modifies the default endpoint so the client can produce a different +# mtls endpoint for endpoint testing purposes. +def modify_default_endpoint(client): + return ( + "foo.googleapis.com" + if ("localhost" in client.DEFAULT_ENDPOINT) + else client.DEFAULT_ENDPOINT + ) + + +def test__get_default_mtls_endpoint(): + api_endpoint = "example.googleapis.com" + api_mtls_endpoint = "example.mtls.googleapis.com" + sandbox_endpoint = "example.sandbox.googleapis.com" + sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" + non_googleapi = "api.example.com" + + assert DocumentProcessorServiceClient._get_default_mtls_endpoint(None) is None + assert ( + DocumentProcessorServiceClient._get_default_mtls_endpoint(api_endpoint) + == api_mtls_endpoint + ) + assert ( + DocumentProcessorServiceClient._get_default_mtls_endpoint(api_mtls_endpoint) + == api_mtls_endpoint + ) + assert ( + DocumentProcessorServiceClient._get_default_mtls_endpoint(sandbox_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + DocumentProcessorServiceClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + DocumentProcessorServiceClient._get_default_mtls_endpoint(non_googleapi) + == non_googleapi + ) + + +@pytest.mark.parametrize( + "client_class", + [DocumentProcessorServiceClient, DocumentProcessorServiceAsyncClient,], +) +def test_document_processor_service_client_from_service_account_info(client_class): + creds = credentials.AnonymousCredentials() + with mock.patch.object( + service_account.Credentials, "from_service_account_info" + ) as factory: + factory.return_value = creds + info = {"valid": True} + client = client_class.from_service_account_info(info) + assert client.transport._credentials == creds + assert isinstance(client, client_class) + + assert client.transport._host == "us-documentai.googleapis.com:443" + + +@pytest.mark.parametrize( + "client_class", + [DocumentProcessorServiceClient, DocumentProcessorServiceAsyncClient,], +) +def test_document_processor_service_client_from_service_account_file(client_class): + creds = credentials.AnonymousCredentials() + with mock.patch.object( + service_account.Credentials, "from_service_account_file" + ) as factory: + factory.return_value = creds + client = client_class.from_service_account_file("dummy/file/path.json") + assert client.transport._credentials == creds + assert isinstance(client, client_class) + + client = client_class.from_service_account_json("dummy/file/path.json") + assert client.transport._credentials == creds + assert isinstance(client, client_class) + + assert client.transport._host == "us-documentai.googleapis.com:443" + + +def test_document_processor_service_client_get_transport_class(): + transport = DocumentProcessorServiceClient.get_transport_class() + available_transports = [ + transports.DocumentProcessorServiceGrpcTransport, + ] + assert transport in available_transports + + transport = DocumentProcessorServiceClient.get_transport_class("grpc") + assert transport == transports.DocumentProcessorServiceGrpcTransport + + +@pytest.mark.parametrize( + "client_class,transport_class,transport_name", + [ + ( + DocumentProcessorServiceClient, + transports.DocumentProcessorServiceGrpcTransport, + "grpc", + ), + ( + DocumentProcessorServiceAsyncClient, + transports.DocumentProcessorServiceGrpcAsyncIOTransport, + "grpc_asyncio", + ), + ], +) +@mock.patch.object( + DocumentProcessorServiceClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(DocumentProcessorServiceClient), +) +@mock.patch.object( + DocumentProcessorServiceAsyncClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(DocumentProcessorServiceAsyncClient), +) +def test_document_processor_service_client_client_options( + client_class, transport_class, transport_name +): + # Check that if channel is provided we won't create a new one. + with mock.patch.object( + DocumentProcessorServiceClient, "get_transport_class" + ) as gtc: + transport = transport_class(credentials=credentials.AnonymousCredentials()) + client = client_class(transport=transport) + gtc.assert_not_called() + + # Check that if channel is provided via str we will create a new one. + with mock.patch.object( + DocumentProcessorServiceClient, "get_transport_class" + ) as gtc: + client = client_class(transport=transport_name) + gtc.assert_called() + + # Check the case api_endpoint is provided. + options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + 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="squid.clam.whelk", + scopes=None, + client_cert_source_for_mtls=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_ENDPOINT is + # "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() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=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_ENDPOINT is + # "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() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_MTLS_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=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_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, + client_cert_source_for_mtls=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", + [ + ( + DocumentProcessorServiceClient, + transports.DocumentProcessorServiceGrpcTransport, + "grpc", + "true", + ), + ( + DocumentProcessorServiceAsyncClient, + transports.DocumentProcessorServiceGrpcAsyncIOTransport, + "grpc_asyncio", + "true", + ), + ( + DocumentProcessorServiceClient, + transports.DocumentProcessorServiceGrpcTransport, + "grpc", + "false", + ), + ( + DocumentProcessorServiceAsyncClient, + transports.DocumentProcessorServiceGrpcAsyncIOTransport, + "grpc_asyncio", + "false", + ), + ], +) +@mock.patch.object( + DocumentProcessorServiceClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(DocumentProcessorServiceClient), +) +@mock.patch.object( + DocumentProcessorServiceAsyncClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(DocumentProcessorServiceAsyncClient), +) +@mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}) +def test_document_processor_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) + + if use_client_cert_env == "false": + expected_client_cert_source = None + expected_host = client.DEFAULT_ENDPOINT + else: + expected_client_cert_source = client_cert_source_callback + expected_host = client.DEFAULT_MTLS_ENDPOINT + + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=expected_host, + scopes=None, + client_cert_source_for_mtls=expected_client_cert_source, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) + + # 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=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=client_cert_source_callback, + ): + if use_client_cert_env == "false": + expected_host = client.DEFAULT_ENDPOINT + expected_client_cert_source = None + else: + expected_host = client.DEFAULT_MTLS_ENDPOINT + expected_client_cert_source = client_cert_source_callback + + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=expected_host, + scopes=None, + client_cert_source_for_mtls=expected_client_cert_source, + 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.mtls.has_default_client_cert_source", + 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, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) + + +@pytest.mark.parametrize( + "client_class,transport_class,transport_name", + [ + ( + DocumentProcessorServiceClient, + transports.DocumentProcessorServiceGrpcTransport, + "grpc", + ), + ( + DocumentProcessorServiceAsyncClient, + transports.DocumentProcessorServiceGrpcAsyncIOTransport, + "grpc_asyncio", + ), + ], +) +def test_document_processor_service_client_client_options_scopes( + client_class, transport_class, transport_name +): + # Check the case scopes are provided. + options = client_options.ClientOptions(scopes=["1", "2"],) + 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=["1", "2"], + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) + + +@pytest.mark.parametrize( + "client_class,transport_class,transport_name", + [ + ( + DocumentProcessorServiceClient, + transports.DocumentProcessorServiceGrpcTransport, + "grpc", + ), + ( + DocumentProcessorServiceAsyncClient, + transports.DocumentProcessorServiceGrpcAsyncIOTransport, + "grpc_asyncio", + ), + ], +) +def test_document_processor_service_client_client_options_credentials_file( + client_class, transport_class, transport_name +): + # Check the case credentials file is provided. + options = client_options.ClientOptions(credentials_file="credentials.json") + 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="credentials.json", + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) + + +def test_document_processor_service_client_client_options_from_dict(): + with mock.patch( + "google.cloud.documentai_v1.services.document_processor_service.transports.DocumentProcessorServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = DocumentProcessorServiceClient( + client_options={"api_endpoint": "squid.clam.whelk"} + ) + grpc_transport.assert_called_once_with( + credentials=None, + credentials_file=None, + host="squid.clam.whelk", + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) + + +def test_process_document( + transport: str = "grpc", request_type=document_processor_service.ProcessRequest +): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.process_document), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = document_processor_service.ProcessResponse() + + response = client.process_document(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + + assert args[0] == document_processor_service.ProcessRequest() + + # Establish that the response is the type that we expect. + + assert isinstance(response, document_processor_service.ProcessResponse) + + +def test_process_document_from_dict(): + test_process_document(request_type=dict) + + +def test_process_document_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.process_document), "__call__") as call: + client.process_document() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == document_processor_service.ProcessRequest() + + +@pytest.mark.asyncio +async def test_process_document_async( + transport: str = "grpc_asyncio", + request_type=document_processor_service.ProcessRequest, +): + client = DocumentProcessorServiceAsyncClient( + credentials=credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.process_document), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + document_processor_service.ProcessResponse() + ) + + response = await client.process_document(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + + assert args[0] == document_processor_service.ProcessRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, document_processor_service.ProcessResponse) + + +@pytest.mark.asyncio +async def test_process_document_async_from_dict(): + await test_process_document_async(request_type=dict) + + +def test_process_document_field_headers(): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = document_processor_service.ProcessRequest() + request.name = "name/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.process_document), "__call__") as call: + call.return_value = document_processor_service.ProcessResponse() + + client.process_document(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=name/value",) in kw["metadata"] + + +@pytest.mark.asyncio +async def test_process_document_field_headers_async(): + client = DocumentProcessorServiceAsyncClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = document_processor_service.ProcessRequest() + request.name = "name/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.process_document), "__call__") as call: + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + document_processor_service.ProcessResponse() + ) + + await client.process_document(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=name/value",) in kw["metadata"] + + +def test_process_document_flattened(): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.process_document), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = document_processor_service.ProcessResponse() + + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + client.process_document(name="name_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + + assert args[0].name == "name_value" + + +def test_process_document_flattened_error(): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + client.process_document( + document_processor_service.ProcessRequest(), name="name_value", + ) + + +@pytest.mark.asyncio +async def test_process_document_flattened_async(): + client = DocumentProcessorServiceAsyncClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.process_document), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = document_processor_service.ProcessResponse() + + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + document_processor_service.ProcessResponse() + ) + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + response = await client.process_document(name="name_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + + assert args[0].name == "name_value" + + +@pytest.mark.asyncio +async def test_process_document_flattened_error_async(): + client = DocumentProcessorServiceAsyncClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + await client.process_document( + document_processor_service.ProcessRequest(), name="name_value", + ) + + +def test_batch_process_documents( + transport: str = "grpc", request_type=document_processor_service.BatchProcessRequest +): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.batch_process_documents), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/spam") + + response = client.batch_process_documents(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + + assert args[0] == document_processor_service.BatchProcessRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, future.Future) + + +def test_batch_process_documents_from_dict(): + test_batch_process_documents(request_type=dict) + + +def test_batch_process_documents_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.batch_process_documents), "__call__" + ) as call: + client.batch_process_documents() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == document_processor_service.BatchProcessRequest() + + +@pytest.mark.asyncio +async def test_batch_process_documents_async( + transport: str = "grpc_asyncio", + request_type=document_processor_service.BatchProcessRequest, +): + client = DocumentProcessorServiceAsyncClient( + credentials=credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.batch_process_documents), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/spam") + ) + + response = await client.batch_process_documents(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + + assert args[0] == document_processor_service.BatchProcessRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, future.Future) + + +@pytest.mark.asyncio +async def test_batch_process_documents_async_from_dict(): + await test_batch_process_documents_async(request_type=dict) + + +def test_batch_process_documents_field_headers(): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = document_processor_service.BatchProcessRequest() + request.name = "name/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.batch_process_documents), "__call__" + ) as call: + call.return_value = operations_pb2.Operation(name="operations/op") + + client.batch_process_documents(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=name/value",) in kw["metadata"] + + +@pytest.mark.asyncio +async def test_batch_process_documents_field_headers_async(): + client = DocumentProcessorServiceAsyncClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = document_processor_service.BatchProcessRequest() + request.name = "name/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.batch_process_documents), "__call__" + ) as call: + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/op") + ) + + await client.batch_process_documents(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=name/value",) in kw["metadata"] + + +def test_batch_process_documents_flattened(): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.batch_process_documents), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/op") + + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + client.batch_process_documents(name="name_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + + assert args[0].name == "name_value" + + +def test_batch_process_documents_flattened_error(): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + client.batch_process_documents( + document_processor_service.BatchProcessRequest(), name="name_value", + ) + + +@pytest.mark.asyncio +async def test_batch_process_documents_flattened_async(): + client = DocumentProcessorServiceAsyncClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.batch_process_documents), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/op") + + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/spam") + ) + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + response = await client.batch_process_documents(name="name_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + + assert args[0].name == "name_value" + + +@pytest.mark.asyncio +async def test_batch_process_documents_flattened_error_async(): + client = DocumentProcessorServiceAsyncClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + await client.batch_process_documents( + document_processor_service.BatchProcessRequest(), name="name_value", + ) + + +def test_review_document( + transport: str = "grpc", + request_type=document_processor_service.ReviewDocumentRequest, +): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.review_document), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/spam") + + response = client.review_document(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + + assert args[0] == document_processor_service.ReviewDocumentRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, future.Future) + + +def test_review_document_from_dict(): + test_review_document(request_type=dict) + + +def test_review_document_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.review_document), "__call__") as call: + client.review_document() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == document_processor_service.ReviewDocumentRequest() + + +@pytest.mark.asyncio +async def test_review_document_async( + transport: str = "grpc_asyncio", + request_type=document_processor_service.ReviewDocumentRequest, +): + client = DocumentProcessorServiceAsyncClient( + credentials=credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.review_document), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/spam") + ) + + response = await client.review_document(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + + assert args[0] == document_processor_service.ReviewDocumentRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, future.Future) + + +@pytest.mark.asyncio +async def test_review_document_async_from_dict(): + await test_review_document_async(request_type=dict) + + +def test_review_document_field_headers(): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = document_processor_service.ReviewDocumentRequest() + request.human_review_config = "human_review_config/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.review_document), "__call__") as call: + call.return_value = operations_pb2.Operation(name="operations/op") + + client.review_document(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ( + "x-goog-request-params", + "human_review_config=human_review_config/value", + ) in kw["metadata"] + + +@pytest.mark.asyncio +async def test_review_document_field_headers_async(): + client = DocumentProcessorServiceAsyncClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = document_processor_service.ReviewDocumentRequest() + request.human_review_config = "human_review_config/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.review_document), "__call__") as call: + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/op") + ) + + await client.review_document(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ( + "x-goog-request-params", + "human_review_config=human_review_config/value", + ) in kw["metadata"] + + +def test_review_document_flattened(): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.review_document), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/op") + + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + client.review_document(human_review_config="human_review_config_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + + assert args[0].human_review_config == "human_review_config_value" + + +def test_review_document_flattened_error(): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + client.review_document( + document_processor_service.ReviewDocumentRequest(), + human_review_config="human_review_config_value", + ) + + +@pytest.mark.asyncio +async def test_review_document_flattened_async(): + client = DocumentProcessorServiceAsyncClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.review_document), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/op") + + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/spam") + ) + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + response = await client.review_document( + human_review_config="human_review_config_value", + ) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + + assert args[0].human_review_config == "human_review_config_value" + + +@pytest.mark.asyncio +async def test_review_document_flattened_error_async(): + client = DocumentProcessorServiceAsyncClient( + credentials=credentials.AnonymousCredentials(), + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + await client.review_document( + document_processor_service.ReviewDocumentRequest(), + human_review_config="human_review_config_value", + ) + + +def test_credentials_transport_error(): + # It is an error to provide credentials and a transport instance. + transport = transports.DocumentProcessorServiceGrpcTransport( + credentials=credentials.AnonymousCredentials(), + ) + with pytest.raises(ValueError): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), transport=transport, + ) + + # It is an error to provide a credentials file and a transport instance. + transport = transports.DocumentProcessorServiceGrpcTransport( + credentials=credentials.AnonymousCredentials(), + ) + with pytest.raises(ValueError): + client = DocumentProcessorServiceClient( + client_options={"credentials_file": "credentials.json"}, + transport=transport, + ) + + # It is an error to provide scopes and a transport instance. + transport = transports.DocumentProcessorServiceGrpcTransport( + credentials=credentials.AnonymousCredentials(), + ) + with pytest.raises(ValueError): + client = DocumentProcessorServiceClient( + client_options={"scopes": ["1", "2"]}, transport=transport, + ) + + +def test_transport_instance(): + # A client may be instantiated with a custom transport instance. + transport = transports.DocumentProcessorServiceGrpcTransport( + credentials=credentials.AnonymousCredentials(), + ) + client = DocumentProcessorServiceClient(transport=transport) + assert client.transport is transport + + +def test_transport_get_channel(): + # A client may be instantiated with a custom transport instance. + transport = transports.DocumentProcessorServiceGrpcTransport( + credentials=credentials.AnonymousCredentials(), + ) + channel = transport.grpc_channel + assert channel + + transport = transports.DocumentProcessorServiceGrpcAsyncIOTransport( + credentials=credentials.AnonymousCredentials(), + ) + channel = transport.grpc_channel + assert channel + + +@pytest.mark.parametrize( + "transport_class", + [ + transports.DocumentProcessorServiceGrpcTransport, + transports.DocumentProcessorServiceGrpcAsyncIOTransport, + ], +) +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 = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), + ) + assert isinstance( + client.transport, transports.DocumentProcessorServiceGrpcTransport, + ) + + +def test_document_processor_service_base_transport_error(): + # Passing both a credentials object and credentials_file should raise an error + with pytest.raises(exceptions.DuplicateCredentialArgs): + transport = transports.DocumentProcessorServiceTransport( + credentials=credentials.AnonymousCredentials(), + credentials_file="credentials.json", + ) + + +def test_document_processor_service_base_transport(): + # Instantiate the base transport. + with mock.patch( + "google.cloud.documentai_v1.services.document_processor_service.transports.DocumentProcessorServiceTransport.__init__" + ) as Transport: + Transport.return_value = None + transport = transports.DocumentProcessorServiceTransport( + credentials=credentials.AnonymousCredentials(), + ) + + # Every method on the transport should just blindly + # raise NotImplementedError. + methods = ( + "process_document", + "batch_process_documents", + "review_document", + ) + for method in methods: + with pytest.raises(NotImplementedError): + getattr(transport, method)(request=object()) + + # Additionally, the LRO client (a property) should + # also raise NotImplementedError + with pytest.raises(NotImplementedError): + transport.operations_client + + +def test_document_processor_service_base_transport_with_credentials_file(): + # Instantiate the base transport with a credentials file + with mock.patch.object( + auth, "load_credentials_from_file" + ) as load_creds, mock.patch( + "google.cloud.documentai_v1.services.document_processor_service.transports.DocumentProcessorServiceTransport._prep_wrapped_messages" + ) as Transport: + Transport.return_value = None + load_creds.return_value = (credentials.AnonymousCredentials(), None) + transport = transports.DocumentProcessorServiceTransport( + credentials_file="credentials.json", quota_project_id="octopus", + ) + load_creds.assert_called_once_with( + "credentials.json", + scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id="octopus", + ) + + +def test_document_processor_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.documentai_v1.services.document_processor_service.transports.DocumentProcessorServiceTransport._prep_wrapped_messages" + ) as Transport: + Transport.return_value = None + adc.return_value = (credentials.AnonymousCredentials(), None) + transport = transports.DocumentProcessorServiceTransport() + adc.assert_called_once() + + +def test_document_processor_service_auth_adc(): + # If no credentials are provided, we should use ADC credentials. + with mock.patch.object(auth, "default") as adc: + adc.return_value = (credentials.AnonymousCredentials(), None) + DocumentProcessorServiceClient() + adc.assert_called_once_with( + scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id=None, + ) + + +def test_document_processor_service_transport_auth_adc(): + # If credentials and host are not provided, the transport class should use + # ADC credentials. + with mock.patch.object(auth, "default") as adc: + adc.return_value = (credentials.AnonymousCredentials(), None) + transports.DocumentProcessorServiceGrpcTransport( + host="squid.clam.whelk", quota_project_id="octopus" + ) + adc.assert_called_once_with( + scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id="octopus", + ) + + +@pytest.mark.parametrize( + "transport_class", + [ + transports.DocumentProcessorServiceGrpcTransport, + transports.DocumentProcessorServiceGrpcAsyncIOTransport, + ], +) +def test_document_processor_service_grpc_transport_client_cert_source_for_mtls( + transport_class, +): + cred = credentials.AnonymousCredentials() + + # Check ssl_channel_credentials is used if provided. + with mock.patch.object(transport_class, "create_channel") as mock_create_channel: + mock_ssl_channel_creds = mock.Mock() + transport_class( + host="squid.clam.whelk", + credentials=cred, + ssl_channel_credentials=mock_ssl_channel_creds, + ) + mock_create_channel.assert_called_once_with( + "squid.clam.whelk:443", + credentials=cred, + credentials_file=None, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ssl_credentials=mock_ssl_channel_creds, + quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + # Check if ssl_channel_credentials is not provided, then client_cert_source_for_mtls + # is used. + with mock.patch.object(transport_class, "create_channel", return_value=mock.Mock()): + with mock.patch("grpc.ssl_channel_credentials") as mock_ssl_cred: + transport_class( + credentials=cred, + client_cert_source_for_mtls=client_cert_source_callback, + ) + expected_cert, expected_key = client_cert_source_callback() + mock_ssl_cred.assert_called_once_with( + certificate_chain=expected_cert, private_key=expected_key + ) + + +def test_document_processor_service_host_no_port(): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), + client_options=client_options.ClientOptions( + api_endpoint="us-documentai.googleapis.com" + ), + ) + assert client.transport._host == "us-documentai.googleapis.com:443" + + +def test_document_processor_service_host_with_port(): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), + client_options=client_options.ClientOptions( + api_endpoint="us-documentai.googleapis.com:8000" + ), + ) + assert client.transport._host == "us-documentai.googleapis.com:8000" + + +def test_document_processor_service_grpc_transport_channel(): + channel = grpc.secure_channel("http://localhost/", grpc.local_channel_credentials()) + + # Check that channel is used if provided. + transport = transports.DocumentProcessorServiceGrpcTransport( + host="squid.clam.whelk", channel=channel, + ) + assert transport.grpc_channel == channel + assert transport._host == "squid.clam.whelk:443" + assert transport._ssl_channel_credentials == None + + +def test_document_processor_service_grpc_asyncio_transport_channel(): + channel = aio.secure_channel("http://localhost/", grpc.local_channel_credentials()) + + # Check that channel is used if provided. + transport = transports.DocumentProcessorServiceGrpcAsyncIOTransport( + host="squid.clam.whelk", channel=channel, + ) + assert transport.grpc_channel == channel + assert transport._host == "squid.clam.whelk:443" + assert transport._ssl_channel_credentials == None + + +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. +@pytest.mark.parametrize( + "transport_class", + [ + transports.DocumentProcessorServiceGrpcTransport, + transports.DocumentProcessorServiceGrpcAsyncIOTransport, + ], +) +def test_document_processor_service_transport_channel_mtls_with_client_cert_source( + transport_class, +): + with mock.patch( + "grpc.ssl_channel_credentials", autospec=True + ) as grpc_ssl_channel_cred: + with mock.patch.object( + transport_class, "create_channel" + ) 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",), + ssl_credentials=mock_ssl_cred, + quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + assert transport.grpc_channel == mock_grpc_channel + assert transport._ssl_channel_credentials == mock_ssl_cred + + +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. +@pytest.mark.parametrize( + "transport_class", + [ + transports.DocumentProcessorServiceGrpcTransport, + transports.DocumentProcessorServiceGrpcAsyncIOTransport, + ], +) +def test_document_processor_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), + ): + with mock.patch.object( + transport_class, "create_channel" + ) 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",), + ssl_credentials=mock_ssl_cred, + quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + assert transport.grpc_channel == mock_grpc_channel + + +def test_document_processor_service_grpc_lro_client(): + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + transport = client.transport + + # Ensure that we have a api-core operations client. + assert isinstance(transport.operations_client, operations_v1.OperationsClient,) + + # Ensure that subsequent calls to the property send the exact same object. + assert transport.operations_client is transport.operations_client + + +def test_document_processor_service_grpc_lro_async_client(): + client = DocumentProcessorServiceAsyncClient( + credentials=credentials.AnonymousCredentials(), transport="grpc_asyncio", + ) + transport = client.transport + + # Ensure that we have a api-core operations client. + assert isinstance(transport.operations_client, operations_v1.OperationsAsyncClient,) + + # Ensure that subsequent calls to the property send the exact same object. + assert transport.operations_client is transport.operations_client + + +def test_human_review_config_path(): + project = "squid" + location = "clam" + processor = "whelk" + + expected = "projects/{project}/locations/{location}/processors/{processor}/humanReviewConfig".format( + project=project, location=location, processor=processor, + ) + actual = DocumentProcessorServiceClient.human_review_config_path( + project, location, processor + ) + assert expected == actual + + +def test_parse_human_review_config_path(): + expected = { + "project": "octopus", + "location": "oyster", + "processor": "nudibranch", + } + path = DocumentProcessorServiceClient.human_review_config_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_human_review_config_path(path) + assert expected == actual + + +def test_processor_path(): + project = "cuttlefish" + location = "mussel" + processor = "winkle" + + expected = "projects/{project}/locations/{location}/processors/{processor}".format( + project=project, location=location, processor=processor, + ) + actual = DocumentProcessorServiceClient.processor_path(project, location, processor) + assert expected == actual + + +def test_parse_processor_path(): + expected = { + "project": "nautilus", + "location": "scallop", + "processor": "abalone", + } + path = DocumentProcessorServiceClient.processor_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_processor_path(path) + assert expected == actual + + +def test_common_billing_account_path(): + billing_account = "squid" + + expected = "billingAccounts/{billing_account}".format( + billing_account=billing_account, + ) + actual = DocumentProcessorServiceClient.common_billing_account_path(billing_account) + assert expected == actual + + +def test_parse_common_billing_account_path(): + expected = { + "billing_account": "clam", + } + path = DocumentProcessorServiceClient.common_billing_account_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_common_billing_account_path(path) + assert expected == actual + + +def test_common_folder_path(): + folder = "whelk" + + expected = "folders/{folder}".format(folder=folder,) + actual = DocumentProcessorServiceClient.common_folder_path(folder) + assert expected == actual + + +def test_parse_common_folder_path(): + expected = { + "folder": "octopus", + } + path = DocumentProcessorServiceClient.common_folder_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_common_folder_path(path) + assert expected == actual + + +def test_common_organization_path(): + organization = "oyster" + + expected = "organizations/{organization}".format(organization=organization,) + actual = DocumentProcessorServiceClient.common_organization_path(organization) + assert expected == actual + + +def test_parse_common_organization_path(): + expected = { + "organization": "nudibranch", + } + path = DocumentProcessorServiceClient.common_organization_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_common_organization_path(path) + assert expected == actual + + +def test_common_project_path(): + project = "cuttlefish" + + expected = "projects/{project}".format(project=project,) + actual = DocumentProcessorServiceClient.common_project_path(project) + assert expected == actual + + +def test_parse_common_project_path(): + expected = { + "project": "mussel", + } + path = DocumentProcessorServiceClient.common_project_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_common_project_path(path) + assert expected == actual + + +def test_common_location_path(): + project = "winkle" + location = "nautilus" + + expected = "projects/{project}/locations/{location}".format( + project=project, location=location, + ) + actual = DocumentProcessorServiceClient.common_location_path(project, location) + assert expected == actual + + +def test_parse_common_location_path(): + expected = { + "project": "scallop", + "location": "abalone", + } + path = DocumentProcessorServiceClient.common_location_path(**expected) + + # Check that the path construction is reversible. + actual = DocumentProcessorServiceClient.parse_common_location_path(path) + assert expected == actual + + +def test_client_withDEFAULT_CLIENT_INFO(): + client_info = gapic_v1.client_info.ClientInfo() + + with mock.patch.object( + transports.DocumentProcessorServiceTransport, "_prep_wrapped_messages" + ) as prep: + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), client_info=client_info, + ) + prep.assert_called_once_with(client_info) + + with mock.patch.object( + transports.DocumentProcessorServiceTransport, "_prep_wrapped_messages" + ) as prep: + transport_class = DocumentProcessorServiceClient.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/documentai_v1beta2/__init__.py b/tests/unit/gapic/documentai_v1beta2/__init__.py index 8b137891..42ffdf2b 100644 --- a/tests/unit/gapic/documentai_v1beta2/__init__.py +++ b/tests/unit/gapic/documentai_v1beta2/__init__.py @@ -1 +1,16 @@ +# -*- coding: utf-8 -*- +# 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. +# diff --git a/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py b/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py index 3c617faa..ffea17d6 100644 --- a/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py +++ b/tests/unit/gapic/documentai_v1beta2/test_document_understanding_service.py @@ -99,15 +99,20 @@ def test__get_default_mtls_endpoint(): ) -def test_document_understanding_service_client_from_service_account_info(): +@pytest.mark.parametrize( + "client_class", + [DocumentUnderstandingServiceClient, DocumentUnderstandingServiceAsyncClient,], +) +def test_document_understanding_service_client_from_service_account_info(client_class): creds = credentials.AnonymousCredentials() with mock.patch.object( service_account.Credentials, "from_service_account_info" ) as factory: factory.return_value = creds info = {"valid": True} - client = DocumentUnderstandingServiceClient.from_service_account_info(info) + client = client_class.from_service_account_info(info) assert client.transport._credentials == creds + assert isinstance(client, client_class) assert client.transport._host == "us-documentai.googleapis.com:443" @@ -124,9 +129,11 @@ def test_document_understanding_service_client_from_service_account_file(client_ factory.return_value = creds client = client_class.from_service_account_file("dummy/file/path.json") assert client.transport._credentials == creds + assert isinstance(client, client_class) client = client_class.from_service_account_json("dummy/file/path.json") assert client.transport._credentials == creds + assert isinstance(client, client_class) assert client.transport._host == "us-documentai.googleapis.com:443" @@ -513,6 +520,24 @@ def test_batch_process_documents_from_dict(): test_batch_process_documents(request_type=dict) +def test_batch_process_documents_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = DocumentUnderstandingServiceClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.batch_process_documents), "__call__" + ) as call: + client.batch_process_documents() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == document_understanding.BatchProcessDocumentsRequest() + + @pytest.mark.asyncio async def test_batch_process_documents_async( transport: str = "grpc_asyncio", @@ -747,6 +772,22 @@ def test_process_document_from_dict(): test_process_document(request_type=dict) +def test_process_document_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = DocumentUnderstandingServiceClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.process_document), "__call__") as call: + client.process_document() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == document_understanding.ProcessDocumentRequest() + + @pytest.mark.asyncio async def test_process_document_async( transport: str = "grpc_asyncio", diff --git a/tests/unit/gapic/documentai_v1beta3/__init__.py b/tests/unit/gapic/documentai_v1beta3/__init__.py index 8b137891..42ffdf2b 100644 --- a/tests/unit/gapic/documentai_v1beta3/__init__.py +++ b/tests/unit/gapic/documentai_v1beta3/__init__.py @@ -1 +1,16 @@ +# -*- coding: utf-8 -*- +# 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. +# diff --git a/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py b/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py index 68a00a85..7179689c 100644 --- a/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py +++ b/tests/unit/gapic/documentai_v1beta3/test_document_processor_service.py @@ -45,6 +45,7 @@ transports, ) from google.cloud.documentai_v1beta3.types import document +from google.cloud.documentai_v1beta3.types import document_io from google.cloud.documentai_v1beta3.types import document_processor_service from google.cloud.documentai_v1beta3.types import geometry from google.longrunning import operations_pb2 @@ -106,15 +107,20 @@ def test__get_default_mtls_endpoint(): ) -def test_document_processor_service_client_from_service_account_info(): +@pytest.mark.parametrize( + "client_class", + [DocumentProcessorServiceClient, DocumentProcessorServiceAsyncClient,], +) +def test_document_processor_service_client_from_service_account_info(client_class): creds = credentials.AnonymousCredentials() with mock.patch.object( service_account.Credentials, "from_service_account_info" ) as factory: factory.return_value = creds info = {"valid": True} - client = DocumentProcessorServiceClient.from_service_account_info(info) + client = client_class.from_service_account_info(info) assert client.transport._credentials == creds + assert isinstance(client, client_class) assert client.transport._host == "us-documentai.googleapis.com:443" @@ -131,9 +137,11 @@ def test_document_processor_service_client_from_service_account_file(client_clas factory.return_value = creds client = client_class.from_service_account_file("dummy/file/path.json") assert client.transport._credentials == creds + assert isinstance(client, client_class) client = client_class.from_service_account_json("dummy/file/path.json") assert client.transport._credentials == creds + assert isinstance(client, client_class) assert client.transport._host == "us-documentai.googleapis.com:443" @@ -522,6 +530,22 @@ def test_process_document_from_dict(): test_process_document(request_type=dict) +def test_process_document_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.process_document), "__call__") as call: + client.process_document() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == document_processor_service.ProcessRequest() + + @pytest.mark.asyncio async def test_process_document_async( transport: str = "grpc_asyncio", @@ -727,6 +751,24 @@ def test_batch_process_documents_from_dict(): test_batch_process_documents(request_type=dict) +def test_batch_process_documents_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.batch_process_documents), "__call__" + ) as call: + client.batch_process_documents() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == document_processor_service.BatchProcessRequest() + + @pytest.mark.asyncio async def test_batch_process_documents_async( transport: str = "grpc_asyncio", @@ -937,6 +979,22 @@ def test_review_document_from_dict(): test_review_document(request_type=dict) +def test_review_document_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = DocumentProcessorServiceClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.review_document), "__call__") as call: + client.review_document() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == document_processor_service.ReviewDocumentRequest() + + @pytest.mark.asyncio async def test_review_document_async( transport: str = "grpc_asyncio", From a75c42a5f660a19b7950d5374196cf908173137c Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 25 Mar 2021 15:50:03 +0000 Subject: [PATCH 30/30] chore: release 0.4.0 (#57) :robot: I have created a release \*beep\* \*boop\* --- ## [0.4.0](https://www.github.com/googleapis/python-documentai/compare/v0.3.0...v0.4.0) (2021-03-25) ### Features * add 'from_service_account_info' factory to clients ([d6f183a](https://www.github.com/googleapis/python-documentai/commit/d6f183a696b211c6d29bc28e9bbd0a8537f65577)) * add common resource path helpers, expose client transport ([#43](https://www.github.com/googleapis/python-documentai/issues/43)) ([4918e62](https://www.github.com/googleapis/python-documentai/commit/4918e62033b4c118bf99ba83730377b4ecc86d17)) * add documentai v1 ([#101](https://www.github.com/googleapis/python-documentai/issues/101)) ([74fabb5](https://www.github.com/googleapis/python-documentai/commit/74fabb5e260ecc27e9cf005502d79590fa7f72e4)) * add from_service_account_info factory and fix sphinx identifiers ([#80](https://www.github.com/googleapis/python-documentai/issues/80)) ([d6f183a](https://www.github.com/googleapis/python-documentai/commit/d6f183a696b211c6d29bc28e9bbd0a8537f65577)) ### Bug Fixes * added if statement to filter out dir blob files ([#63](https://www.github.com/googleapis/python-documentai/issues/63)) ([7f7f541](https://www.github.com/googleapis/python-documentai/commit/7f7f541bcf4d2f42b2f619c2ceb45f53c5d0e9eb)) * adds comment with explicit hostname change ([#94](https://www.github.com/googleapis/python-documentai/issues/94)) ([bb639f9](https://www.github.com/googleapis/python-documentai/commit/bb639f9470304b9c408143a3e8091a4ca8c54160)) * fix sphinx identifiers ([d6f183a](https://www.github.com/googleapis/python-documentai/commit/d6f183a696b211c6d29bc28e9bbd0a8537f65577)) * moves import statment inside region tags ([#71](https://www.github.com/googleapis/python-documentai/issues/71)) ([a04fbea](https://www.github.com/googleapis/python-documentai/commit/a04fbeaf026d3d204dbb6c6cecf181068ddcc882)) * remove client recv msg limit and add enums to `types/__init__.py` ([#72](https://www.github.com/googleapis/python-documentai/issues/72)) ([c94afd5](https://www.github.com/googleapis/python-documentai/commit/c94afd55124b0abc8978bf86b84743dd4afb0778)) * removes C-style semicolons and slash comments ([#59](https://www.github.com/googleapis/python-documentai/issues/59)) ([1b24bfd](https://www.github.com/googleapis/python-documentai/commit/1b24bfdfc603952db8d1c633dfde108a396aa707)) * **samples:** swaps 'continue' for 'return' ([#93](https://www.github.com/googleapis/python-documentai/issues/93)) ([dabe48e](https://www.github.com/googleapis/python-documentai/commit/dabe48e8c1439ceb8a50c18aa3c7dca848a9117a)) ### Documentation * fix pypi link ([#46](https://www.github.com/googleapis/python-documentai/issues/46)) ([5162674](https://www.github.com/googleapis/python-documentai/commit/5162674091b9a2111b90eb26739b4e11f9119582)) * **samples:** new Doc AI samples for v1beta3 ([#44](https://www.github.com/googleapis/python-documentai/issues/44)) ([cc8c58d](https://www.github.com/googleapis/python-documentai/commit/cc8c58d1bade4be53fde08f6a3497eb3f79f63b1)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 839b914e..fa0af008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [0.4.0](https://www.github.com/googleapis/python-documentai/compare/v0.3.0...v0.4.0) (2021-03-25) + + +### Features + +* add 'from_service_account_info' factory to clients ([d6f183a](https://www.github.com/googleapis/python-documentai/commit/d6f183a696b211c6d29bc28e9bbd0a8537f65577)) +* add common resource path helpers, expose client transport ([#43](https://www.github.com/googleapis/python-documentai/issues/43)) ([4918e62](https://www.github.com/googleapis/python-documentai/commit/4918e62033b4c118bf99ba83730377b4ecc86d17)) +* add documentai v1 ([#101](https://www.github.com/googleapis/python-documentai/issues/101)) ([74fabb5](https://www.github.com/googleapis/python-documentai/commit/74fabb5e260ecc27e9cf005502d79590fa7f72e4)) +* add from_service_account_info factory and fix sphinx identifiers ([#80](https://www.github.com/googleapis/python-documentai/issues/80)) ([d6f183a](https://www.github.com/googleapis/python-documentai/commit/d6f183a696b211c6d29bc28e9bbd0a8537f65577)) + + +### Bug Fixes + +* added if statement to filter out dir blob files ([#63](https://www.github.com/googleapis/python-documentai/issues/63)) ([7f7f541](https://www.github.com/googleapis/python-documentai/commit/7f7f541bcf4d2f42b2f619c2ceb45f53c5d0e9eb)) +* adds comment with explicit hostname change ([#94](https://www.github.com/googleapis/python-documentai/issues/94)) ([bb639f9](https://www.github.com/googleapis/python-documentai/commit/bb639f9470304b9c408143a3e8091a4ca8c54160)) +* fix sphinx identifiers ([d6f183a](https://www.github.com/googleapis/python-documentai/commit/d6f183a696b211c6d29bc28e9bbd0a8537f65577)) +* moves import statment inside region tags ([#71](https://www.github.com/googleapis/python-documentai/issues/71)) ([a04fbea](https://www.github.com/googleapis/python-documentai/commit/a04fbeaf026d3d204dbb6c6cecf181068ddcc882)) +* remove client recv msg limit and add enums to `types/__init__.py` ([#72](https://www.github.com/googleapis/python-documentai/issues/72)) ([c94afd5](https://www.github.com/googleapis/python-documentai/commit/c94afd55124b0abc8978bf86b84743dd4afb0778)) +* removes C-style semicolons and slash comments ([#59](https://www.github.com/googleapis/python-documentai/issues/59)) ([1b24bfd](https://www.github.com/googleapis/python-documentai/commit/1b24bfdfc603952db8d1c633dfde108a396aa707)) +* **samples:** swaps 'continue' for 'return' ([#93](https://www.github.com/googleapis/python-documentai/issues/93)) ([dabe48e](https://www.github.com/googleapis/python-documentai/commit/dabe48e8c1439ceb8a50c18aa3c7dca848a9117a)) + + +### Documentation + +* fix pypi link ([#46](https://www.github.com/googleapis/python-documentai/issues/46)) ([5162674](https://www.github.com/googleapis/python-documentai/commit/5162674091b9a2111b90eb26739b4e11f9119582)) +* **samples:** new Doc AI samples for v1beta3 ([#44](https://www.github.com/googleapis/python-documentai/issues/44)) ([cc8c58d](https://www.github.com/googleapis/python-documentai/commit/cc8c58d1bade4be53fde08f6a3497eb3f79f63b1)) + ## [0.3.0](https://www.github.com/googleapis/python-documentai/compare/v0.2.0...v0.3.0) (2020-09-30) diff --git a/setup.py b/setup.py index fac3d0ca..4cba6749 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ import os import setuptools # type: ignore -version = "0.3.0" +version = "0.4.0" package_root = os.path.abspath(os.path.dirname(__file__)) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy