Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save vchudnov-g/c035d5407cb0d485d0f81da2e58dbf6c to your computer and use it in GitHub Desktop.
Save vchudnov-g/c035d5407cb0d485d0f81da2e58dbf6c to your computer and use it in GitHub Desktop.
Vision Partial GAPICs & you! πŸ¦‡

πŸ–‹ GAPIC :: Library Partials

So you want to write a partial helper, eh?

Hand-written code adds all kinds of functionality on top of generated GAPIC client libraries!

Let's look at the most common type of hand-written extension:

>> Helper Methods added to service object which wraps call to 1 rpc method <<



"for demonstration purposes only"

πŸ‘ Cloud Vision API

The Google Cloud Vision API is a great candidate for this because, while the API supports performing various types of image analysis, there is actually only 1 rpc method available. To perform certain types of analysis, the rpc request contains an array of enum values, each representing 1 type of analysis. 1 or many types may be passed at once but, often, folks may want to easily invoke just 1 type of analysis!

Here is an example of a Request to detect all of the Landmarks in a given image (How-to Guide):

ImageAnnotatorService.BatchAnnotateImages(
  BatchAnnotateImagesRequest {
    requests = [
        AnnotateImageRequest {
          image = Image {
            source = ImageSource { image_uri = "gs://bucket/path/to/an/image.png" }
          }
          features = [
            Feature { type = Feature.Type.LANDMARK_DETECTION }
          ]
        }
    ]
  }
)

And here is what the code looks like in Python:

uri_for_image_stored_in_cloud_storage = "gs://cloud-samples-tests/vision/landmark.jpg"

from google.cloud import vision

client = vision.ImageAnnotatorClient()

response = client.batch_annotate_images(
    requests = [
        vision.types.AnnotateImageRequest(
            image = vision.types.Image(
                source = vision.types.ImageSource(
                    gcs_image_uri = uri_for_image_stored_in_cloud_storage    
                )
            ),
            features = [
                vision.types.Feature(
                    type = vision.enums.Feature.Type.LANDMARK_DETECTION
                )
            ]
        )
    ]
)

print(response)

Skip to the FUN PART below 😎


Wouldn't it be great if we could simply call client.landmark_detection(myImage)?

Well, you can! It already exists! Check out the current Landmark Detection Python Sample and you'll see it looks like this:

image = vision.types.Image()
image.source.image_uri = uri
response = client.landmark_detection(image=image)

If you're interested in how that helper method is added today, you can checkout the vision_helpers/ folder in the Python client library for Google Cloud Vision.

tl;dr

  1. add_single_feature_methods loops over the available Feature enum values
  2. For each Feature, it dynamically creates a function via _create_single_feature_method and dynamically adds that function to the VisionHelpers class
  3. Each dynamically created function for each enum value calls the helper method annotate_image, which invokes BatchAnnotateImage with a single request (rather than an array of requests)
  4. The VisionHelpers class is inherited by the ImageAnnotatorClient class using multiple inheritance & @add_single_feature_methods is invoked as an decorator.
@add_single_feature_methods
class ImageAnnotatorClient(VisionHelpers, iac.ImageAnnotatorClient):

^-- some hand-wavy guessing, I'm not actually familiar with Python's inheritance model or how Python decorators work :P

But that's not what we're here to do. We're here to generate helper methods across all languages.

So let's do so by misusing the sample generation features... 😈


Misusing Samplegen! 😎

We all know why we're here. (πŸ‘‹πŸ¦‡)

Let's repurpose our snippets as helper methods, shall we?

Round 1

I'm not going to actually add the function to VisionHelpers or anything, I'm just going to generate a top-level method called def detect_landmarks which only accepts a GCS URI and returns the API response.

We won't accept any other kwargs** to pass along or anything, like we should look into really doing (the dynamically generated helper represents the request as a Dictionary with splatted kwargs**: here)

I replaced the Python sample above with the following (and my goal was to make it work):

from generated_helper import detect_landmarks

response = detect_landmarks("gs://cloud-samples-tests/vision/landmark.jpg")

print(response)

... and so I did.

Script

Below is a hacky script that does all the hacky things necessary to make it go!

  1. clone a fresh gapic-generator (because I hack the .snip)
  2. patch gapic-generator with snip.diff to hack the existing standalone snippet template
    • remove region tags
    • remove print output
    • remove commented out defaults
    • provide a name for the function in the YAML (abusing unused title config field)
    • return response
  3. compile gapic-generator
  4. clone a fresh googleapis (because I represent the helper method as a sample)
  5. patch googleapis with detect_landmarks.diff (adds a "sample" representing the helper method to the Vision V1 GAPIC config)
  6. pull artman docker image
  7. generate Python "sample" (ie. def detect_landmarks()) and save it in the local directory as generated_helpers.py

Then, you can run it.

  1. pip install google-cloud-vision
  2. run the above python example which calls detect_landmarks()

It should give you the exact same output as the full length sample which calls batch_annotate_images() directly


πŸ‘©πŸΌβ€πŸ’» the 🌎

gapic-generator/
googleapis/
#! /bin/bash
# dependencies: git, java, docker
# repos will be cloned here:
working_directory="`pwd`"
# the .diff patches may only apply cleanly to the current HEAD
# which the patches were generated from... we'll checkout those SHAs
gapic_generator_latest=a4d600a1c3bb526ce47feca2a9f832a25694f3bd
googleapis_latest=caa431d9ddb71a29b14ff6bfa6ccd7c044cf9697
gapic_generator_root="$working_directory/gapic-generator"
googleapis_root="$working_directory/googleapis"
set -ex
# nuke the existing gapic-generator / googleapis clones in the working dir
#
# rm -rf <------
#
rm -rf "$gapic_generator_root"
rm -rf "$googleapis_root"
git clone https://github.com/googleapis/gapic-generator.git
git clone https://github.com/googleapis/googleapis.git
# Update GAPIC generator .snip (hack) to render function && compile
cd "$gapic_generator_root"
git checkout $gapic_generator_latest
git status
git apply < "$working_directory/snip.diff"
git status
./gradlew fatJar
# Add the "function" to googleapis
cd "$googleapis_root"
git checkout $googleapis_latest
git status
git apply < "$working_directory/detect_landmarks.diff"
git status
# Sweet. Let's go back 🏑
cd "$working_directory"
# Let's pull down the latest docker image and run it!
docker pull googleapis/artman
docker run --rm \
-e RUNNING_IN_ARTMAN_DOCKER=True \
-v "$gapic_generator_root:/toolkit" \
-v "$googleapis_root:/googleapis" \
-w /googleapis \
googleapis/artman \
/bin/bash -c \
"artman --local --config /googleapis/google/cloud/vision/artman_vision_v1.yaml generate python_gapic"
# If all went as planned, there is a new Python file with the function we want:
cp "$googleapis_root/artman-genfiles/python/vision-v1/samples/google/cloud/vision_v1/gapic/batch_annotate_images/batch_annotate_images_request_landmark_detection_sample.py" generated_helper.py
echo "[ generated_helper.pyh ]"
cat generated_helper.py
echo "Would you like to try to run the code now?"
echo "This requires having GOOGLE_APPLICATION_CREDENTIALS set"
echo "And I'm assuming you have virtualenv and Python 3"
echo
echo "Press any key to continue (Ctrl-C to exit)"
virtualenv --python python3 env
source env/bin/activate
pip install -r requirements.txt
echo "OK, let's run this..."
echo
echo "Hold onto your butts πŸ¦–"
python example_using_helper.py
diff --git a/google/cloud/vision/v1/vision_gapic.yaml b/google/cloud/vision/v1/vision_gapic.yaml
index d95e1a82b..b72eeba14 100644
--- a/google/cloud/vision/v1/vision_gapic.yaml
+++ b/google/cloud/vision/v1/vision_gapic.yaml
@@ -48,6 +48,21 @@ interfaces:
total_timeout_millis: 600000
methods:
- name: BatchAnnotateImages
+ samples:
+ standalone:
+ - calling_forms: ".*"
+ value_sets: ["landmark_detection_sample"]
+ sample_value_sets:
+ - id: landmark_detection_sample
+ title: detect_landmarks
+ description: "Performs Landmark Detection on provided GCS image URI"
+ parameters:
+ defaults:
+ - requests[0].image.source.gcs_image_uri="gs://cloud-samples-tests/vision/landmark.jpg"
+ - requests[0].features[0].type=LANDMARK_DETECTION
+ attributes:
+ - parameter: requests[0].image.source.gcs_image_uri
+ sample_argument_name: cloud_storage_uri
flattening:
groups:
- parameters:
uri_for_image_stored_in_cloud_storage = "gs://cloud-samples-tests/vision/landmark.jpg"
from google.cloud import vision
client = vision.ImageAnnotatorClient()
response = client.batch_annotate_images(
requests = [
vision.types.AnnotateImageRequest(
image = vision.types.Image(
source = vision.types.ImageSource(
gcs_image_uri = uri_for_image_stored_in_cloud_storage
)
),
features = [
vision.types.Feature(
type = vision.enums.Feature.Type.LANDMARK_DETECTION
)
]
)
]
)
print(response)
from generated_helper import detect_landmarks
response = detect_landmarks("gs://cloud-samples-tests/vision/landmark.jpg")
print(response)
# -*- coding: utf-8 -*-
#
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# DO NOT EDIT! This is a generated sample ("Request", "landmark_detection_sample")
# To install the latest published package dependency, execute the following:
# pip install google-cloud-vision
import sys
from google.cloud import vision_v1
from google.cloud.vision_v1 import enums
import six
def detect_landmarks(gcs_image_uri):
"""Performs Landmark Detection on provided GCS image URI"""
client = vision_v1.ImageAnnotatorClient()
source = {'gcs_image_uri': gcs_image_uri}
image = {'source': source}
type_ = enums.Feature.Type.LANDMARK_DETECTION
features_element = {'type': type_}
features = [features_element]
requests_element = {'image': image, 'features': features}
requests = [requests_element]
response = client.batch_annotate_images(requests)
return response
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
'--gcs_image_uri',
type=str,
default='gs://cloud-samples-tests/vision/landmark.jpg')
args = parser.parse_args()
sample_batch_annotate_images(args.gcs_image_uri)
if __name__ == '__main__':
main()
google-cloud-vision==0.34.0
diff --git a/src/main/resources/com/google/api/codegen/py/standalone_sample.snip b/src/main/resources/com/google/api/codegen/py/standalone_sample.snip
index 92ebfbc0..fd200007 100644
--- a/src/main/resources/com/google/api/codegen/py/standalone_sample.snip
+++ b/src/main/resources/com/google/api/codegen/py/standalone_sample.snip
@@ -22,8 +22,6 @@
@snippet standaloneSample(apiMethod, sample)
import sys
- @# [START {@sample.regionTag}]
-
{@importList(sample.initCode.importSection.appImports)}
@if sample.outputImports.size
{@importList(sample.outputImports)}
@@ -34,22 +32,11 @@
@end
- def {@sample.sampleFunctionName}({@formalArgs(sample.initCode.argDefaultParams)}):
+ def {@sample.valueSet.title}({@formalArgs(sample.initCode.argDefaultParams)}):
"""{@sample.valueSet.description}"""
- @# [START {@sample.regionTag}_core]
-
client = {@apiMethod.apiModuleName}.{@apiMethod.apiClassName}()
- @if sample.initCode.argDefaultLines
- @join line : util.pretty(initCode(sample.initCode.argDefaultLines))
- @# {@line}
- @end
-
-
- # FIXME(hzyi): handle ListInitType, MapInitType and StructInitType correctly
- {@convertTextCode}
- @end
@if sample.initCode.lines
{@initCode(sample.initCode.lines)}
@@ -73,8 +60,7 @@
$unhandledCallingForm: {@sample.callingForm} in sample "{@apiMethod.getClass.getSimpleName}"$
@end
- @# [END {@sample.regionTag}_core]
- @# [END {@sample.regionTag}]
+ return response
@end
def main():
@@ -209,11 +195,11 @@
@end
@private processResponse(sample)
- @if sample.outputs.size == 0
- print(response)
- @else
- {@processOutputViews(sample.outputs)}
- @end
+ # @if sample.outputs.size == 0
+ # print(response)
+ # @else
+ # {@processOutputViews(sample.outputs)}
+ # @end
@end
@private processOutputViews(outputViews)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment