diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2ca758af79b..6fcc03d77f8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -55,6 +55,7 @@ /monitoring/opencensus @yuriatgoogle @GoogleCloudPlatform/python-samples-reviewers /monitoring/prometheus @yuriatgoogle @GoogleCloudPlatform/python-samples-reviewers /notebooks/**/* @alixhami @GoogleCloudPlatform/python-samples-reviewers +/optimization/**/* @GoogleCloudPlatform/dee-data-ai @GoogleCloudPlatform/python-samples-reviewers /opencensus/**/* @GoogleCloudPlatform/python-samples-reviewers /people-and-planet-ai/**/* @davidcavazos @GoogleCloudPlatform/python-samples-reviewers /profiler/**/* @GoogleCloudPlatform/python-samples-reviewers diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml index 0bc1e8c4bc2..a087893a796 100644 --- a/.github/blunderbuss.yml +++ b/.github/blunderbuss.yml @@ -86,6 +86,10 @@ assign_issues_by: - 'api: notebooks' to: - alixhami +- labels: + - 'api: optimization' + to: + - GoogleCloudPlatform/dee-data-ai - labels: - 'api: people-and-planet-ai' to: diff --git a/optimization/snippets/async_api.py b/optimization/snippets/async_api.py new file mode 100644 index 00000000000..8ab95b939c4 --- /dev/null +++ b/optimization/snippets/async_api.py @@ -0,0 +1,59 @@ +# Copyright 2022 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 cloudoptimization_async_api] + +from google.api_core.exceptions import GoogleAPICallError +from google.cloud import optimization_v1 + +# TODO(developer): Uncomment these variables before running the sample. +# project_id= 'YOUR_PROJECT_ID' +# request_file_name = 'YOUR_REQUEST_FILE_NAME' +# request_model_gcs_path = 'gs://YOUR_PROJECT/YOUR_BUCKET/YOUR_REQUEST_MODEL_PATH' +# model_solution_gcs_path = 'gs://YOUR_PROJECT/YOUR_BUCKET/YOUR_SOLUCTION_PATH' + + +def call_async_api( + project_id: str, request_model_gcs_path: str, model_solution_gcs_path_prefix: str +) -> None: + """Call the async api for fleet routing.""" + # Use the default credentials for the environment to authenticate the client. + fleet_routing_client = optimization_v1.FleetRoutingClient() + request_file_name = "resources/async_request.json" + + with open(request_file_name, "r") as f: + fleet_routing_request = optimization_v1.BatchOptimizeToursRequest.from_json( + f.read() + ) + fleet_routing_request.parent = f"projects/{project_id}" + for idx, mc in enumerate(fleet_routing_request.model_configs): + mc.input_config.gcs_source.uri = request_model_gcs_path + model_solution_gcs_path = f"{model_solution_gcs_path_prefix}_{idx}" + mc.output_config.gcs_destination.uri = model_solution_gcs_path + + # The timeout argument for the gRPC call is independent from the `timeout` + # field in the request's OptimizeToursRequest message(s). + operation = fleet_routing_client.batch_optimize_tours(fleet_routing_request) + print(operation.operation.name) + + try: + # Block to wait for the job to finish. + result = operation.result() + print(result) + # Do you stuff. + except GoogleAPICallError: + print(operation.operation.error) + + +# [END cloudoptimization_async_api] diff --git a/optimization/snippets/async_api_test.py b/optimization/snippets/async_api_test.py new file mode 100644 index 00000000000..fce1b585f7c --- /dev/null +++ b/optimization/snippets/async_api_test.py @@ -0,0 +1,49 @@ +# Copyright 2022 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 uuid + +import google.auth +from google.cloud import storage +import pytest + +import async_api + + +# TODO(developer): Replace the variables in the file before use. +# A sample request model can be found at resources/async_request_model.json. +TEST_UUID = uuid.uuid4() +BUCKET = f"optimization-ai-{TEST_UUID}" +OUTPUT_PREFIX = f"code_snippets_test_output_{TEST_UUID}" +INPUT_URI = "gs://cloud-samples-data/optimization-ai/async_request_model.json" +BATCH_OUTPUT_URI_PREFIX = "gs://{}/{}/".format(BUCKET, OUTPUT_PREFIX) + + +@pytest.fixture(autouse=True) +def setup_teardown() -> None: + """Create a temporary bucket to store optimization output.""" + storage_client = storage.Client() + bucket = storage_client.create_bucket(BUCKET) + + yield + + bucket.delete(force=True) + + +def test_call_async_api(capsys: pytest.LogCaptureFixture) -> None: + _, project_id = google.auth.default() + async_api.call_async_api(project_id, INPUT_URI, BATCH_OUTPUT_URI_PREFIX) + out, _ = capsys.readouterr() + + assert "operations" in out diff --git a/optimization/snippets/get_operation.py b/optimization/snippets/get_operation.py new file mode 100644 index 00000000000..7f20b96c515 --- /dev/null +++ b/optimization/snippets/get_operation.py @@ -0,0 +1,34 @@ +# Copyright 2022 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 cloudoptimization_get_operation] +from google.cloud import optimization_v1 + + +def get_operation(operation_full_id: str) -> None: + """Get operation details and status.""" + # TODO(developer): Uncomment and set the following variables + # operation_full_id = \ + # "projects/[projectId]/operations/[operationId]" + + client = optimization_v1.FleetRoutingClient() + # Get the latest state of a long-running operation. + response = client.transport.operations_client.get_operation(operation_full_id) + + print("Name: {}".format(response.name)) + print("Operation details:") + print(response) + + +# [END cloudoptimization_get_operation] diff --git a/optimization/snippets/get_operation_test.py b/optimization/snippets/get_operation_test.py new file mode 100644 index 00000000000..e942e444bbd --- /dev/null +++ b/optimization/snippets/get_operation_test.py @@ -0,0 +1,41 @@ +# Copyright 2022 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 google.auth +from google.cloud import optimization_v1 +import pytest + +import get_operation + + +@pytest.fixture(scope="function") +def operation_id() -> str: + fleet_routing_client = optimization_v1.FleetRoutingClient() + + _, project_id = google.auth.default() + fleet_routing_request = {"parent": f"projects/{project_id}"} + + # Make the request + operation = fleet_routing_client.batch_optimize_tours(fleet_routing_request) + + yield operation.operation.name + + +def test_get_operation_status( + capsys: pytest.LogCaptureFixture, operation_id: str +) -> None: + get_operation.get_operation(operation_id) + out, _ = capsys.readouterr() + assert "Operation details" in out diff --git a/optimization/snippets/noxfile_config.py b/optimization/snippets/noxfile_config.py new file mode 100644 index 00000000000..545546d21cb --- /dev/null +++ b/optimization/snippets/noxfile_config.py @@ -0,0 +1,42 @@ +# 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 +# +# 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. + +# Default TEST_CONFIG_OVERRIDE for python repos. + +# You can copy this file into your directory, then it will be imported from +# the noxfile.py. + +# The source of truth: +# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py + +TEST_CONFIG_OVERRIDE = { + # You can opt out from the test for specific Python versions. + "ignored_versions": ["2.7", "3.6"], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": True, + # 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', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} diff --git a/optimization/snippets/requirements-test.txt b/optimization/snippets/requirements-test.txt new file mode 100644 index 00000000000..19a897665b9 --- /dev/null +++ b/optimization/snippets/requirements-test.txt @@ -0,0 +1,2 @@ +pytest==7.2.0 + diff --git a/optimization/snippets/requirements.txt b/optimization/snippets/requirements.txt new file mode 100644 index 00000000000..ae1d796f972 --- /dev/null +++ b/optimization/snippets/requirements.txt @@ -0,0 +1,2 @@ +google-cloud-optimization==1.1.3 +google-cloud-storage==2.5.0 diff --git a/optimization/snippets/resources/async_request.json b/optimization/snippets/resources/async_request.json new file mode 100644 index 00000000000..10ae1b0cc25 --- /dev/null +++ b/optimization/snippets/resources/async_request.json @@ -0,0 +1,33 @@ +{ + "parent": "projects/${YOUR_GCP_PROJECT_ID}", + "model_configs":[ + { + "input_config":{ + "gcs_source":{ + "uri":"${REQUEST_MODEL_GCS_PATH}" + }, + "data_format":"JSON" + }, + "output_config":{ + "gcs_destination":{ + "uri":"${MODEL_SOLUTION_GCS_PATH}" + }, + "data_format":"JSON" + } + }, + { + "input_config":{ + "gcs_source":{ + "uri":"${REQUEST_MODEL_GCS_PATH}" + }, + "data_format":"JSON" + }, + "output_config":{ + "gcs_destination":{ + "uri":"${MODEL_SOLUTION_GCS_PATH}" + }, + "data_format":"JSON" + } + } + ] + } \ No newline at end of file diff --git a/optimization/snippets/resources/async_request_model.json b/optimization/snippets/resources/async_request_model.json new file mode 100644 index 00000000000..75c4b111be4 --- /dev/null +++ b/optimization/snippets/resources/async_request_model.json @@ -0,0 +1,114 @@ +{ + "parent":"${YOUR_GCP_PROJECT_ID}", + "allowLargeDeadlineDespiteInterruptionRisk":true, + "model":{ + "shipments":[ + { + "deliveries":[ + { + "arrivalLocation":{ + "latitude":48.880941999999997, + "longitude":2.3238660000000002 + }, + "duration":"250s", + "timeWindows":[ + { + "endTime":"1970-01-01T01:06:40Z", + "startTime":"1970-01-01T00:50:00Z" + } + ] + } + ], + "loadDemands": { + "weight": { + "amount": "10" + } + }, + "pickups":[ + { + "arrivalLocation":{ + "latitude":48.874507000000001, + "longitude":2.3036099999999999 + }, + "duration":"150s", + "timeWindows":[ + { + "endTime":"1970-01-01T00:33:20Z", + "startTime":"1970-01-01T00:16:40Z" + } + ] + } + ] + }, + { + "deliveries":[ + { + "arrivalLocation":{ + "latitude":48.880940000000002, + "longitude":2.3238439999999998 + }, + "duration":"251s", + "timeWindows":[ + { + "endTime":"1970-01-01T01:06:41Z", + "startTime":"1970-01-01T00:50:01Z" + } + ] + } + ], + "loadDemands": { + "weight": { + "amount": "20" + } + }, + "pickups":[ + { + "arrivalLocation":{ + "latitude":48.880943000000002, + "longitude":2.3238669999999999 + }, + "duration":"151s", + "timeWindows":[ + { + "endTime":"1970-01-01T00:33:21Z", + "startTime":"1970-01-01T00:16:41Z" + } + ] + } + ] + } + ], + "vehicles":[ + { + "loadLimits": { + "weight": { + "maxLoad": 50 + } + }, + "endLocation":{ + "latitude":48.863109999999999, + "longitude":2.341205 + }, + "startLocation":{ + "latitude":48.863101999999998, + "longitude":2.3412039999999998 + } + }, + { + "loadLimits": { + "weight": { + "maxLoad": 60 + } + }, + "endLocation":{ + "latitude":48.863120000000002, + "longitude":2.341215 + }, + "startLocation":{ + "latitude":48.863112000000001, + "longitude":2.3412139999999999 + } + } + ] + } + } \ No newline at end of file diff --git a/optimization/snippets/resources/sync_request.json b/optimization/snippets/resources/sync_request.json new file mode 100644 index 00000000000..cbdf7474ed7 --- /dev/null +++ b/optimization/snippets/resources/sync_request.json @@ -0,0 +1,114 @@ +{ + "parent": "projects/${YOUR_GCP_PROJECT_ID}", + "timeout": "15s", + "model": { + "shipments": [ + { + "deliveries": [ + { + "arrivalLocation": { + "latitude": 48.880942, + "longitude": 2.323866 + }, + "duration": "250s", + "timeWindows": [ + { + "endTime": "1970-01-01T01:06:40Z", + "startTime": "1970-01-01T00:50:00Z" + } + ] + } + ], + "loadDemands": { + "weight": { + "amount": "10" + } + }, + "pickups": [ + { + "arrivalLocation": { + "latitude": 48.874507, + "longitude": 2.30361 + }, + "duration": "150s", + "timeWindows": [ + { + "endTime": "1970-01-01T00:33:20Z", + "startTime": "1970-01-01T00:16:40Z" + } + ] + } + ] + }, + { + "deliveries": [ + { + "arrivalLocation": { + "latitude": 48.88094, + "longitude": 2.323844 + }, + "duration": "251s", + "timeWindows": [ + { + "endTime": "1970-01-01T01:06:41Z", + "startTime": "1970-01-01T00:50:01Z" + } + ] + } + ], + "loadDemands": { + "weight": { + "amount": "20" + } + }, + "pickups": [ + { + "arrivalLocation": { + "latitude": 48.880943, + "longitude": 2.323867 + }, + "duration": "151s", + "timeWindows": [ + { + "endTime": "1970-01-01T00:33:21Z", + "startTime": "1970-01-01T00:16:41Z" + } + ] + } + ] + } + ], + "vehicles": [ + { + "loadLimits": { + "weight": { + "maxLoad": 50 + } + }, + "endLocation": { + "latitude": 48.86311, + "longitude": 2.341205 + }, + "startLocation": { + "latitude": 48.863102, + "longitude": 2.341204 + } + }, + { + "loadLimits": { + "weight": { + "maxLoad": 60 + } + }, + "endLocation": { + "latitude": 48.86312, + "longitude": 2.341215 + }, + "startLocation": { + "latitude": 48.863112, + "longitude": 2.341214 + } + } + ] + } +} \ No newline at end of file diff --git a/optimization/snippets/sync_api.py b/optimization/snippets/sync_api.py new file mode 100644 index 00000000000..187afd27099 --- /dev/null +++ b/optimization/snippets/sync_api.py @@ -0,0 +1,47 @@ +# Copyright 2022 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 cloudoptimization_sync_api] + +from google.cloud import optimization_v1 + +# TODO(developer): Uncomment these variables before running the sample. +# project_id= 'YOUR_PROJECT_ID' + + +def call_sync_api(project_id: str) -> None: + """Call the sync api for fleet routing.""" + # Use the default credentials for the environment. + # Change the file name to your request file. + request_file_name = "resources/sync_request.json" + fleet_routing_client = optimization_v1.FleetRoutingClient() + + with open(request_file_name, "r") as f: + # The request must include the `parent` field with the value set to + # 'projects/{YOUR_GCP_PROJECT_ID}'. + fleet_routing_request = optimization_v1.OptimizeToursRequest.from_json(f.read()) + fleet_routing_request.parent = f"projects/{project_id}" + # Send the request and print the response. + # Fleet Routing will return a response by the earliest of the `timeout` + # field in the request payload and the gRPC timeout specified below. + fleet_routing_response = fleet_routing_client.optimize_tours( + fleet_routing_request, timeout=100 + ) + print(fleet_routing_response) + # If you want to format the response to JSON, you can do the following: + # from google.protobuf.json_format import MessageToJson + # json_obj = MessageToJson(fleet_routing_response._pb) + + +# [END cloudoptimization_sync_api] diff --git a/optimization/snippets/sync_api_test.py b/optimization/snippets/sync_api_test.py new file mode 100644 index 00000000000..6bfffc0c7c8 --- /dev/null +++ b/optimization/snippets/sync_api_test.py @@ -0,0 +1,29 @@ +# Copyright 2022 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 google.auth +import pytest + + +import sync_api + + +def test_call_sync_api(capsys: pytest.LogCaptureFixture) -> None: + _, project_id = google.auth.default() + sync_api.call_sync_api(project_id) + out, _ = capsys.readouterr() + + expected_strings = ["routes", "visits", "transitions", "metrics"] + for expected_string in expected_strings: + assert expected_string in out diff --git a/optimization/snippets/sync_api_with_long_timeout.py b/optimization/snippets/sync_api_with_long_timeout.py new file mode 100644 index 00000000000..ba658ff7cbd --- /dev/null +++ b/optimization/snippets/sync_api_with_long_timeout.py @@ -0,0 +1,51 @@ +# Copyright 2022 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 cloudoptimization_long_timeout] + +from google.cloud import optimization_v1 +from google.cloud.optimization_v1.services import fleet_routing +from google.cloud.optimization_v1.services.fleet_routing import transports +from google.cloud.optimization_v1.services.fleet_routing.transports import ( + grpc as fleet_routing_grpc, +) + +# TODO(developer): Uncomment these variables before running the sample. +# project_id= 'YOUR_PROJECT_ID' + + +def long_timeout(request_file_name: str, project_id: str) -> None: + with open(request_file_name, "r") as f: + fleet_routing_request = optimization_v1.OptimizeToursRequest.from_json(f.read()) + fleet_routing_request.parent = f"projects/{project_id}" + + # Create a channel to provide a connection to the Fleet Routing servers with + # custom behavior. The `grpc.keepalive_time_ms` channel argument modifies + # the channel behavior in order to send keep-alive pings every 5 minutes. + channel = fleet_routing_grpc.FleetRoutingGrpcTransport.create_channel( + options=[ + ("grpc.keepalive_time_ms", 500), + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + # Keep-alive pings are sent on the transport. Create the transport using the + # custom channel The transport is essentially a wrapper to the channel. + transport = transports.FleetRoutingGrpcTransport(channel=channel) + client = fleet_routing.client.FleetRoutingClient(transport=transport) + fleet_routing_response = client.optimize_tours(fleet_routing_request) + print(fleet_routing_response) + + +# [END cloudoptimization_long_timeout] diff --git a/optimization/snippets/sync_api_with_long_timeout_test.py b/optimization/snippets/sync_api_with_long_timeout_test.py new file mode 100644 index 00000000000..4955fa47cb4 --- /dev/null +++ b/optimization/snippets/sync_api_with_long_timeout_test.py @@ -0,0 +1,29 @@ +# Copyright 2022 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 google.auth +import pytest + +import sync_api_with_long_timeout + + +def test_long_timeout(capsys: pytest.LogCaptureFixture) -> None: + request_file_name = "resources/sync_request.json" + _, project_id = google.auth.default() + sync_api_with_long_timeout.long_timeout(request_file_name, project_id) + out, _ = capsys.readouterr() + + expected_strings = ["routes", "visits", "transitions", "metrics"] + for expected_string in expected_strings: + assert expected_string in out