├── .github └── workflows │ └── go.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── assets ├── lenna-face.json ├── lenna-label.json ├── lenna-safe-search.json ├── lenna.jpg ├── pigeon-app.gif ├── pigeon-cmd.gif ├── pigeon-label.json └── pigeon.png ├── client.go ├── client_test.go ├── cmd └── pigeon │ ├── README.md │ ├── flag.go │ ├── flag_test.go │ └── main.go ├── config.go ├── config_test.go ├── credentials ├── application_credentials_provider.go ├── application_credentials_provider_test.go ├── credentials.go ├── credentials_test.go ├── env_provider.go ├── env_provider_test.go ├── example.json ├── static_provider.go └── static_provider_test.go ├── feature.go ├── feature_test.go ├── go.mod ├── go.sum └── pigeon.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.13 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.13 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v1 18 | 19 | - name: Build 20 | run: go build -v ./... 21 | 22 | - name: Test 23 | run: make test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts 2 | 3 | # Created by https://www.gitignore.io/api/go 4 | 5 | ### Go ### 6 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 7 | *.o 8 | *.a 9 | *.so 10 | 11 | # Folders 12 | _obj 13 | _test 14 | 15 | # Architecture specific extensions/prefixes 16 | *.[568vq] 17 | [568vq].out 18 | 19 | *.cgo1.go 20 | *.cgo2.c 21 | _cgo_defun.c 22 | _cgo_gotypes.go 23 | _cgo_export.* 24 | 25 | _testmain.go 26 | 27 | *.exe 28 | *.test 29 | *.prof 30 | 31 | # Output of the go coverage tool, specifically when used with LiteIDE 32 | *.out 33 | 34 | # External packages folder 35 | # vendor/ 36 | 37 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 38 | .glide/ 39 | 40 | # End of https://www.gitignore.io/api/go 41 | # Created by https://www.gitignore.io/api/vim 42 | 43 | ### Vim ### 44 | # swap 45 | [._]*.s[a-v][a-z] 46 | [._]*.sw[a-p] 47 | [._]s[a-v][a-z] 48 | [._]sw[a-p] 49 | # session 50 | Session.vim 51 | # temporary 52 | .netrwhist 53 | *~ 54 | # auto-generated tag files 55 | tags 56 | 57 | # End of https://www.gitignore.io/api/vim 58 | # Created by https://www.gitignore.io/api/linux 59 | 60 | ### Linux ### 61 | *~ 62 | 63 | # temporary files which can be created if a process still has a handle open of a deleted file 64 | .fuse_hidden* 65 | 66 | # KDE directory preferences 67 | .directory 68 | 69 | # Linux trash folder which might appear on any partition or disk 70 | .Trash-* 71 | 72 | # .nfs files are created when an open file is removed but is still being accessed 73 | .nfs* 74 | 75 | # End of https://www.gitignore.io/api/linux 76 | # Created by https://www.gitignore.io/api/osx 77 | 78 | ### OSX ### 79 | *.DS_Store 80 | .AppleDouble 81 | .LSOverride 82 | 83 | # Icon must end with two \r 84 | Icon 85 | 86 | 87 | # Thumbnails 88 | ._* 89 | 90 | # Files that might appear in the root of a volume 91 | .DocumentRevisions-V100 92 | .fseventsd 93 | .Spotlight-V100 94 | .TemporaryItems 95 | .Trashes 96 | .VolumeIcon.icns 97 | .com.apple.timemachine.donotpresent 98 | 99 | # Directories potentially created on remote AFP share 100 | .AppleDB 101 | .AppleDesktop 102 | Network Trash Folder 103 | Temporary Items 104 | .apdisk 105 | 106 | # End of https://www.gitignore.io/api/osx 107 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | **Working on your first Pull Request?** You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github) 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Shintaro Kaneko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOVERSION=$(shell go version) 2 | GOOS=$(word 1,$(subst /, ,$(lastword $(GOVERSION)))) 3 | GOARCH=$(word 2,$(subst /, ,$(lastword $(GOVERSION)))) 4 | TARGET_ONLY_PKGS=$(shell go list ./... 2> /dev/null | grep -v "/vendor/") 5 | IGNORE_DEPS_GOLINT='vendor/.+\.go' 6 | IGNORE_DEPS_GOCYCLO='vendor/.+\.go' 7 | HAVE_GOLINT:=$(shell which golint) 8 | HAVE_GOCYCLO:=$(shell which gocyclo) 9 | HAVE_GHR:=$(shell which ghr) 10 | HAVE_GOX:=$(shell which gox) 11 | PROJECT_REPONAME=$(notdir $(abspath ./)) 12 | PROJECT_USERNAME=$(notdir $(abspath ../)) 13 | OBJS=$(notdir $(TARGETS)) 14 | LDFLAGS=-ldflags="-s -w" 15 | COMMITISH=$(shell git rev-parse HEAD) 16 | ARTIFACTS_DIR=artifacts 17 | TARGETS=$(addprefix github.com/$(PROJECT_USERNAME)/$(PROJECT_REPONAME)/cmd/,pigeon) 18 | VERSION=$(patsubst "%",%,$(lastword $(shell grep 'const Version' pigeon.go))) 19 | 20 | all: $(TARGETS) 21 | 22 | $(TARGETS): 23 | @go install $(LDFLAGS) -v $@ 24 | 25 | .PHONY: build release clean 26 | build: gox 27 | @mkdir -p $(ARTIFACTS_DIR)/$(VERSION) && cd $(ARTIFACTS_DIR)/$(VERSION); \ 28 | gox $(LDFLAGS) $(TARGETS) 29 | 30 | release: ghr verify-github-token build 31 | @ghr -c $(COMMITISH) -u $(PROJECT_USERNAME) -r $(PROJECT_REPONAME) -t $$GITHUB_TOKEN \ 32 | --replace $(VERSION) $(ARTIFACTS_DIR)/$(VERSION) 33 | 34 | clean: 35 | $(RM) -r $(ARTIFACTS_DIR) 36 | 37 | .PHONY: unit lint cyclo test 38 | unit: lint cyclo test 39 | 40 | lint: golint 41 | @echo "go lint" 42 | @lint=`golint ./...`; \ 43 | lint=`echo "$$lint" | grep -E -v -e ${IGNORE_DEPS_GOLINT}`; \ 44 | echo "$$lint"; if [ "$$lint" != "" ]; then exit 1; fi 45 | 46 | cyclo: gocyclo 47 | @echo "gocyclo -over 20" 48 | @cyclo=`gocyclo -over 20 . 2>&1`; \ 49 | cyclo=`echo "$$cyclo" | grep -E -v -e ${IGNORE_DEPS_GOCYCLO}`; \ 50 | echo "$$cyclo"; if [ "$$cyclo" != "" ]; then exit 1; fi 51 | 52 | test: 53 | @go test $(TARGET_ONLY_PKGS) 54 | 55 | .PHONY: verify-github-token 56 | verify-github-token: 57 | @if [ -z "$$GITHUB_TOKEN" ]; then echo '$$GITHUB_TOKEN is required'; exit 1; fi 58 | 59 | .PHONY: golint gocyclo ghr gox 60 | golint: 61 | ifndef HAVE_GOLINT 62 | @echo "Installing linter" 63 | @go get -u golang.org/x/lint/golint 64 | endif 65 | 66 | gocyclo: 67 | ifndef HAVE_GOCYCLO 68 | @echo "Installing gocyclo" 69 | @go get -u github.com/fzipp/gocyclo 70 | endif 71 | 72 | ghr: 73 | ifndef HAVE_GHR 74 | @echo "Installing ghr to upload binaries for release page" 75 | @go get -u github.com/tcnksm/ghr 76 | endif 77 | 78 | gox: 79 | ifndef HAVE_GOX 80 | @echo "Installing gox to build binaries for Go cross compilation" 81 | @go get -u github.com/mitchellh/gox 82 | endif 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pigeon - Google Cloud Vision API on Golang 2 | 3 | [![GoDoc](https://godoc.org/github.com/kaneshin/pigeon?status.svg)](https://godoc.org/github.com/kaneshin/pigeon) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/kaneshin/pigeon)](https://goreportcard.com/report/github.com/kaneshin/pigeon) 5 | 6 | `pigeon` is a service for the Google Cloud Vision API on Golang. 7 | 8 | ## Prerequisite 9 | 10 | You need to export a service account json file to `GOOGLE_APPLICATION_CREDENTIALS` variable. 11 | 12 | ``` 13 | $ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account.json 14 | ``` 15 | 16 | To generate the credentials file, please, refer to [this documentation page](https://cloud.google.com/vision/docs/common/auth#authenticating_with_application_default_credentials) 17 | 18 | ## Installation 19 | 20 | `pigeon` provides the command-line tools. 21 | 22 | ```shell 23 | $ go get github.com/kaneshin/pigeon/cmd/... 24 | ``` 25 | 26 | Make sure that `pigeon` was installed correctly: 27 | 28 | ```shell 29 | $ pigeon -h 30 | ``` 31 | 32 | ## Usage 33 | 34 | ### `pigeon` command 35 | 36 | `pigeon` is available to submit request with external image source (i.e. Google Cloud Storage image location). 37 | 38 | ```shell 39 | # Default Detection is LabelDetection. 40 | $ pigeon assets/lenna.jpg 41 | $ pigeon -face gs://bucket_name/lenna.jpg 42 | $ pigeon -label https://httpbin.org/image/jpeg 43 | ``` 44 | 45 | ![pigeon-cmd](https://raw.githubusercontent.com/kaneshin/pigeon/main/assets/pigeon-cmd.gif) 46 | 47 | ### `pigeon` package 48 | 49 | ```go 50 | import "github.com/kaneshin/pigeon" 51 | import "github.com/kaneshin/pigeon/credentials" 52 | 53 | func main() { 54 | // Initialize vision service by a credentials json. 55 | creds := credentials.NewApplicationCredentials("credentials.json") 56 | 57 | // creds will set a pointer of credentials object using env value of 58 | // "GOOGLE_APPLICATION_CREDENTIALS" if pass empty string to argument. 59 | // creds := credentials.NewApplicationCredentials("") 60 | 61 | config := pigeon.NewConfig().WithCredentials(creds) 62 | 63 | client, err := pigeon.New(config) 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | // To call multiple image annotation requests. 69 | feature := pigeon.NewFeature(pigeon.LabelDetection) 70 | batch, err := client.NewBatchAnnotateImageRequest([]string{"lenna.jpg"}, feature) 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | // Execute the "vision.images.annotate". 76 | res, err := client.ImagesService().Annotate(batch).Do() 77 | if err != nil { 78 | panic(err) 79 | } 80 | 81 | // Marshal annotations from responses 82 | body, _ := json.MarshalIndent(res.Responses, "", " ") 83 | fmt.Println(string(body)) 84 | } 85 | ``` 86 | 87 | #### pigeon.Client 88 | 89 | The `pigeon.Client` is wrapper of the `vision.Service`. 90 | 91 | ```go 92 | // Initialize vision client by a credentials json. 93 | creds := credentials.NewApplicationCredentials("credentials.json") 94 | client, err := pigeon.New(creds) 95 | if err != nil { 96 | panic(err) 97 | } 98 | ``` 99 | 100 | #### vision.Feature 101 | 102 | `vision.Feature` will be applied to `vision.AnnotateImageRequest`. 103 | 104 | ```go 105 | // DetectionType returns a value of detection type. 106 | func DetectionType(d int) string { 107 | switch d { 108 | case TypeUnspecified: 109 | return "TYPE_UNSPECIFIED" 110 | case FaceDetection: 111 | return "FACE_DETECTION" 112 | case LandmarkDetection: 113 | return "LANDMARK_DETECTION" 114 | case LogoDetection: 115 | return "LOGO_DETECTION" 116 | case LabelDetection: 117 | return "LABEL_DETECTION" 118 | case TextDetection: 119 | return "TEXT_DETECTION" 120 | case SafeSearchDetection: 121 | return "SAFE_SEARCH_DETECTION" 122 | case ImageProperties: 123 | return "IMAGE_PROPERTIES" 124 | } 125 | return "" 126 | } 127 | 128 | // Choose detection types 129 | features := []*vision.Feature{ 130 | pigeon.NewFeature(pigeon.FaceDetection), 131 | pigeon.NewFeature(pigeon.LabelDetection), 132 | pigeon.NewFeature(pigeon.ImageProperties), 133 | } 134 | ``` 135 | 136 | #### vision.AnnotateImageRequest 137 | 138 | `vision.AnnotateImageRequest` needs to set the uri of the form `"gs://bucket_name/foo.png"` or byte content of image. 139 | 140 | - Google Cloud Storage 141 | 142 | ```go 143 | src := "gs://bucket_name/lenna.jpg" 144 | req, err := pigeon.NewAnnotateImageSourceRequest(src, features...) 145 | if err != nil { 146 | panic(err) 147 | } 148 | ``` 149 | 150 | - Base64 Encoded String 151 | 152 | ```go 153 | b, err := ioutil.ReadFile(filename) 154 | if err != nil { 155 | panic(err) 156 | } 157 | req, err = pigeon.NewAnnotateImageContentRequest(b, features...) 158 | if err != nil { 159 | panic(err) 160 | } 161 | ``` 162 | 163 | #### Submit the request to the Google Cloud Vision API 164 | 165 | ```go 166 | // To call multiple image annotation requests. 167 | batch, err := client.NewBatchAnnotateImageRequest(list, features()...) 168 | if err != nil { 169 | panic(err) 170 | } 171 | 172 | // Execute the "vision.images.annotate". 173 | res, err := client.ImagesService().Annotate(batch).Do() 174 | if err != nil { 175 | panic(err) 176 | } 177 | ``` 178 | 179 | 180 | ## Example 181 | 182 | ### Pigeon 183 | 184 | ![pigeon](https://raw.githubusercontent.com/kaneshin/pigeon/main/assets/pigeon.png) 185 | 186 | #### input 187 | 188 | ```shell 189 | $ pigeon -label assets/pigeon.png 190 | ``` 191 | 192 | #### output 193 | 194 | ```json 195 | [ 196 | { 197 | "labelAnnotations": [ 198 | { 199 | "description": "bird", 200 | "mid": "/m/015p6", 201 | "score": 0.825656 202 | }, 203 | { 204 | "description": "anatidae", 205 | "mid": "/m/01c_0l", 206 | "score": 0.58264238 207 | } 208 | ] 209 | } 210 | ] 211 | ``` 212 | 213 | 214 | ### Lenna 215 | 216 | ![lenna](https://raw.githubusercontent.com/kaneshin/pigeon/main/assets/lenna.jpg) 217 | 218 | #### input 219 | 220 | ```shell 221 | $ pigeon -safe-search assets/lenna.jpg 222 | ``` 223 | 224 | #### output 225 | 226 | ```json 227 | [ 228 | { 229 | "safeSearchAnnotation": { 230 | "adult": "POSSIBLE", 231 | "medical": "UNLIKELY", 232 | "spoof": "VERY_UNLIKELY", 233 | "violence": "VERY_UNLIKELY" 234 | } 235 | } 236 | ] 237 | ``` 238 | 239 | ## License 240 | 241 | [The MIT License (MIT)](http://kaneshin.mit-license.org/) 242 | 243 | ## Author 244 | 245 | Shintaro Kaneko 246 | -------------------------------------------------------------------------------- /assets/lenna-face.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "faceAnnotations": [ 4 | { 5 | "angerLikelihood": "VERY_UNLIKELY", 6 | "blurredLikelihood": "VERY_UNLIKELY", 7 | "boundingPoly": { 8 | "vertices": [ 9 | { 10 | "x": 143, 11 | "y": 43 12 | }, 13 | { 14 | "x": 245, 15 | "y": 43 16 | }, 17 | { 18 | "x": 245, 19 | "y": 163 20 | }, 21 | { 22 | "x": 143, 23 | "y": 163 24 | } 25 | ] 26 | }, 27 | "detectionConfidence": 0.99805844, 28 | "fdBoundingPoly": { 29 | "vertices": [ 30 | { 31 | "x": 172, 32 | "y": 82 33 | }, 34 | { 35 | "x": 241, 36 | "y": 82 37 | }, 38 | { 39 | "x": 241, 40 | "y": 151 41 | }, 42 | { 43 | "x": 172, 44 | "y": 151 45 | } 46 | ] 47 | }, 48 | "headwearLikelihood": "UNLIKELY", 49 | "joyLikelihood": "VERY_UNLIKELY", 50 | "landmarkingConfidence": 0.5350582, 51 | "landmarks": [ 52 | { 53 | "position": { 54 | "x": 197.90556, 55 | "y": 102.932, 56 | "z": 0.00083794753 57 | }, 58 | "type": "LEFT_EYE" 59 | }, 60 | { 61 | "position": { 62 | "x": 223.43489, 63 | "y": 102.72927, 64 | "z": 17.352478 65 | }, 66 | "type": "RIGHT_EYE" 67 | }, 68 | { 69 | "position": { 70 | "x": 189.50327, 71 | "y": 96.40799, 72 | "z": -4.1362653 73 | }, 74 | "type": "LEFT_OF_LEFT_EYEBROW" 75 | }, 76 | { 77 | "position": { 78 | "x": 209.67989, 79 | "y": 98.01947, 80 | "z": -1.0281576 81 | }, 82 | "type": "RIGHT_OF_LEFT_EYEBROW" 83 | }, 84 | { 85 | "position": { 86 | "x": 222.12822, 87 | "y": 97.624565, 88 | "z": 7.3962259 89 | }, 90 | "type": "LEFT_OF_RIGHT_EYEBROW" 91 | }, 92 | { 93 | "position": { 94 | "x": 232.35756, 95 | "y": 95.693558, 96 | "z": 24.924641 97 | }, 98 | "type": "RIGHT_OF_RIGHT_EYEBROW" 99 | }, 100 | { 101 | "position": { 102 | "x": 216.04073, 103 | "y": 103.35545, 104 | "z": 3.7840507 105 | }, 106 | "type": "MIDPOINT_BETWEEN_EYES" 107 | }, 108 | { 109 | "position": { 110 | "x": 218.52597, 111 | "y": 123.5818, 112 | "z": -0.28893152 113 | }, 114 | "type": "NOSE_TIP" 115 | }, 116 | { 117 | "position": { 118 | "x": 213.84016, 119 | "y": 133.52296, 120 | "z": 7.531785 121 | }, 122 | "type": "UPPER_LIP" 123 | }, 124 | { 125 | "position": { 126 | "x": 211.08179, 127 | "y": 142.36548, 128 | "z": 11.109071 129 | }, 130 | "type": "LOWER_LIP" 131 | }, 132 | { 133 | "position": { 134 | "x": 197.96632, 135 | "y": 135.92284, 136 | "z": 8.2209711 137 | }, 138 | "type": "MOUTH_LEFT" 139 | }, 140 | { 141 | "position": { 142 | "x": 218.75967, 143 | "y": 135.65446, 144 | "z": 21.922234 145 | }, 146 | "type": "MOUTH_RIGHT" 147 | }, 148 | { 149 | "position": { 150 | "x": 211.74457, 151 | "y": 137.08533, 152 | "z": 10.235818 153 | }, 154 | "type": "MOUTH_CENTER" 155 | }, 156 | { 157 | "position": { 158 | "x": 218.9288, 159 | "y": 123.39391, 160 | "z": 14.522773 161 | }, 162 | "type": "NOSE_BOTTOM_RIGHT" 163 | }, 164 | { 165 | "position": { 166 | "x": 206.34598, 167 | "y": 124.65819, 168 | "z": 5.2354078 169 | }, 170 | "type": "NOSE_BOTTOM_LEFT" 171 | }, 172 | { 173 | "position": { 174 | "x": 213.97774, 175 | "y": 127.24593, 176 | "z": 6.3785205 177 | }, 178 | "type": "NOSE_BOTTOM_CENTER" 179 | }, 180 | { 181 | "position": { 182 | "x": 200.3213, 183 | "y": 102.28181, 184 | "z": -1.4001943 185 | }, 186 | "type": "LEFT_EYE_TOP_BOUNDARY" 187 | }, 188 | { 189 | "position": { 190 | "x": 204.84023, 191 | "y": 104.31689, 192 | "z": 4.4483867 193 | }, 194 | "type": "LEFT_EYE_RIGHT_CORNER" 195 | }, 196 | { 197 | "position": { 198 | "x": 198.12639, 199 | "y": 105.78291, 200 | "z": 0.71685004 201 | }, 202 | "type": "LEFT_EYE_BOTTOM_BOUNDARY" 203 | }, 204 | { 205 | "position": { 206 | "x": 192.15016, 207 | "y": 103.58647, 208 | "z": -0.49122569 209 | }, 210 | "type": "LEFT_EYE_LEFT_CORNER" 211 | }, 212 | { 213 | "position": { 214 | "x": 199.04262, 215 | "y": 104.18291, 216 | "z": -0.22829057 217 | }, 218 | "type": "LEFT_EYE_PUPIL" 219 | }, 220 | { 221 | "position": { 222 | "x": 225.94977, 223 | "y": 101.87831, 224 | "z": 16.005444 225 | }, 226 | "type": "RIGHT_EYE_TOP_BOUNDARY" 227 | }, 228 | { 229 | "position": { 230 | "x": 228.10057, 231 | "y": 103.01511, 232 | "z": 23.837673 233 | }, 234 | "type": "RIGHT_EYE_RIGHT_CORNER" 235 | }, 236 | { 237 | "position": { 238 | "x": 224.43965, 239 | "y": 105.74184, 240 | "z": 18.213358 241 | }, 242 | "type": "RIGHT_EYE_BOTTOM_BOUNDARY" 243 | }, 244 | { 245 | "position": { 246 | "x": 219.7216, 247 | "y": 104.20552, 248 | "z": 14.817 249 | }, 250 | "type": "RIGHT_EYE_LEFT_CORNER" 251 | }, 252 | { 253 | "position": { 254 | "x": 225.33717, 255 | "y": 103.78451, 256 | "z": 17.626329 257 | }, 258 | "type": "RIGHT_EYE_PUPIL" 259 | }, 260 | { 261 | "position": { 262 | "x": 201.10205, 263 | "y": 93.874573, 264 | "z": -4.9750185 265 | }, 266 | "type": "LEFT_EYEBROW_UPPER_MIDPOINT" 267 | }, 268 | { 269 | "position": { 270 | "x": 228.88687, 271 | "y": 93.421875, 272 | "z": 13.912791 273 | }, 274 | "type": "RIGHT_EYEBROW_UPPER_MIDPOINT" 275 | }, 276 | { 277 | "position": { 278 | "x": 160.07202, 279 | "y": 111.48654, 280 | "z": 23.152546 281 | }, 282 | "type": "LEFT_EAR_TRAGION" 283 | }, 284 | { 285 | "position": { 286 | "x": 218.2299, 287 | "y": 110.56743, 288 | "z": 62.657734 289 | }, 290 | "type": "RIGHT_EAR_TRAGION" 291 | }, 292 | { 293 | "position": { 294 | "x": 216.65471, 295 | "y": 97.63163, 296 | "z": 2.1570654 297 | }, 298 | "type": "FOREHEAD_GLABELLA" 299 | }, 300 | { 301 | "position": { 302 | "x": 207.27054, 303 | "y": 155.58951, 304 | "z": 17.233084 305 | }, 306 | "type": "CHIN_GNATHION" 307 | }, 308 | { 309 | "position": { 310 | "x": 167.89359, 311 | "y": 133.45166, 312 | "z": 18.304451 313 | }, 314 | "type": "CHIN_LEFT_GONION" 315 | }, 316 | { 317 | "position": { 318 | "x": 220.39294, 319 | "y": 132.60187, 320 | "z": 54.00494 321 | }, 322 | "type": "CHIN_RIGHT_GONION" 323 | } 324 | ], 325 | "panAngle": 34.809193, 326 | "rollAngle": 5.9516206, 327 | "sorrowLikelihood": "VERY_UNLIKELY", 328 | "surpriseLikelihood": "VERY_UNLIKELY", 329 | "tiltAngle": -10.011973, 330 | "underExposedLikelihood": "VERY_UNLIKELY" 331 | } 332 | ] 333 | } 334 | ] 335 | -------------------------------------------------------------------------------- /assets/lenna-label.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "labelAnnotations": [ 4 | { 5 | "description": "clothing", 6 | "mid": "/m/09j2d", 7 | "score": 0.98547381 8 | }, 9 | { 10 | "description": "hair", 11 | "mid": "/m/03q69", 12 | "score": 0.94178885 13 | }, 14 | { 15 | "description": "hairstyle", 16 | "mid": "/m/0ds4x", 17 | "score": 0.85636234 18 | }, 19 | { 20 | "description": "model", 21 | "mid": "/m/0d1pc", 22 | "score": 0.7849946 23 | }, 24 | { 25 | "description": "woman", 26 | "mid": "/m/082lw", 27 | "score": 0.77166933 28 | } 29 | ] 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /assets/lenna-safe-search.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "safeSearchAnnotation": { 4 | "adult": "POSSIBLE", 5 | "medical": "UNLIKELY", 6 | "spoof": "VERY_UNLIKELY", 7 | "violence": "VERY_UNLIKELY" 8 | } 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /assets/lenna.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneshin/pigeon/bd23d773624413c21bd8914f313281f2ae873e06/assets/lenna.jpg -------------------------------------------------------------------------------- /assets/pigeon-app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneshin/pigeon/bd23d773624413c21bd8914f313281f2ae873e06/assets/pigeon-app.gif -------------------------------------------------------------------------------- /assets/pigeon-cmd.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneshin/pigeon/bd23d773624413c21bd8914f313281f2ae873e06/assets/pigeon-cmd.gif -------------------------------------------------------------------------------- /assets/pigeon-label.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "labelAnnotations": [ 4 | { 5 | "description": "bird", 6 | "mid": "/m/015p6", 7 | "score": 0.825656 8 | }, 9 | { 10 | "description": "anatidae", 11 | "mid": "/m/01c_0l", 12 | "score": 0.58264238 13 | } 14 | ] 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /assets/pigeon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaneshin/pigeon/bd23d773624413c21bd8914f313281f2ae873e06/assets/pigeon.png -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package pigeon 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | 12 | "golang.org/x/oauth2/google" 13 | vision "google.golang.org/api/vision/v1" 14 | 15 | "github.com/kaneshin/pigeon/credentials" 16 | ) 17 | 18 | type ( 19 | // A Client provides cloud vision service. 20 | Client struct { 21 | // The context object to use when signing requests. 22 | // Defaults to `context.Background()`. 23 | // context context.Context 24 | 25 | // The Config provides service configuration for service clients. 26 | config *Config 27 | 28 | // The service object. 29 | service *vision.Service 30 | } 31 | ) 32 | 33 | // New returns a pointer to a new Client object. 34 | func New(c *Config, httpClient ...*http.Client) (*Client, error) { 35 | if c == nil { 36 | // Sets a configuration if passed nil value. 37 | c = NewConfig() 38 | } 39 | 40 | // Use HTTP Client if assigned 41 | if len(httpClient) > 0 { 42 | srv, err := vision.New(httpClient[0]) 43 | if err != nil { 44 | return nil, fmt.Errorf("Unable to retrieve vision Client %v", err) 45 | } 46 | return &Client{ 47 | config: c, 48 | service: srv, 49 | }, nil 50 | } 51 | 52 | if c.Credentials == nil { 53 | // Sets application credentials by defaults. 54 | c.Credentials = credentials.NewApplicationCredentials("") 55 | } 56 | 57 | creds, err := c.Credentials.Get() 58 | if err != nil { 59 | return nil, err 60 | } 61 | b, err := json.Marshal(creds) 62 | 63 | config, err := google.JWTConfigFromJSON(b, vision.CloudPlatformScope) 64 | if err != nil { 65 | return nil, fmt.Errorf("Unable to parse client secret file to config: %v", err) 66 | } 67 | client := config.Client(context.Background()) 68 | 69 | srv, err := vision.New(client) 70 | if err != nil { 71 | return nil, fmt.Errorf("Unable to retrieve vision Client %v", err) 72 | } 73 | 74 | return &Client{ 75 | config: c, 76 | service: srv, 77 | }, nil 78 | } 79 | 80 | // ImagesService returns a pointer to a vision's ImagesService object. 81 | func (c Client) ImagesService() *vision.ImagesService { 82 | return c.service.Images 83 | } 84 | 85 | // NewBatchAnnotateImageRequest returns a pointer to a new vision's BatchAnnotateImagesRequest. 86 | func (c Client) NewBatchAnnotateImageRequest(list []string, features ...*vision.Feature) (*vision.BatchAnnotateImagesRequest, error) { 87 | batch := &vision.BatchAnnotateImagesRequest{} 88 | batch.Requests = []*vision.AnnotateImageRequest{} 89 | for _, v := range list { 90 | req, err := c.NewAnnotateImageRequest(v, features...) 91 | if err != nil { 92 | return nil, err 93 | } 94 | batch.Requests = append(batch.Requests, req) 95 | } 96 | return batch, nil 97 | } 98 | 99 | // NewAnnotateImageRequest returns a pointer to a new vision's AnnotateImagesRequest. 100 | func (c Client) NewAnnotateImageRequest(v interface{}, features ...*vision.Feature) (*vision.AnnotateImageRequest, error) { 101 | switch v.(type) { 102 | case []byte: 103 | // base64 104 | return NewAnnotateImageContentRequest(v.([]byte), features...) 105 | case string: 106 | u, err := url.Parse(v.(string)) 107 | if err != nil { 108 | return nil, err 109 | } 110 | switch u.Scheme { 111 | case "gs": 112 | // GcsImageUri: Google Cloud Storage image URI. It must be in the 113 | // following form: 114 | // "gs://bucket_name/object_name". For more 115 | return NewAnnotateImageSourceRequest(u.String(), features...) 116 | case "http", "https": 117 | httpClient := c.config.HTTPClient 118 | if httpClient == nil { 119 | httpClient = http.DefaultClient 120 | } 121 | resp, err := httpClient.Get(u.String()) 122 | if err != nil { 123 | return nil, err 124 | } 125 | defer resp.Body.Close() 126 | if resp.StatusCode >= http.StatusBadRequest { 127 | return nil, http.ErrMissingFile 128 | } 129 | body, err := ioutil.ReadAll(resp.Body) 130 | if err != nil { 131 | return nil, err 132 | } 133 | return c.NewAnnotateImageRequest(body, features...) 134 | } 135 | // filepath 136 | b, err := ioutil.ReadFile(v.(string)) 137 | if err != nil { 138 | return nil, err 139 | } 140 | return c.NewAnnotateImageRequest(b, features...) 141 | } 142 | return &vision.AnnotateImageRequest{}, nil 143 | } 144 | 145 | // NewAnnotateImageContentRequest returns a pointer to a new vision's AnnotateImagesRequest. 146 | func NewAnnotateImageContentRequest(body []byte, features ...*vision.Feature) (*vision.AnnotateImageRequest, error) { 147 | req := &vision.AnnotateImageRequest{ 148 | Image: NewAnnotateImageContent(body), 149 | Features: features, 150 | } 151 | return req, nil 152 | } 153 | 154 | // NewAnnotateImageSourceRequest returns a pointer to a new vision's AnnotateImagesRequest. 155 | func NewAnnotateImageSourceRequest(source string, features ...*vision.Feature) (*vision.AnnotateImageRequest, error) { 156 | req := &vision.AnnotateImageRequest{ 157 | Image: NewAnnotateImageSource(source), 158 | Features: features, 159 | } 160 | return req, nil 161 | } 162 | 163 | // NewAnnotateImageContent returns a pointer to a new vision's Image. 164 | // It's contained image content, represented as a stream of bytes. 165 | func NewAnnotateImageContent(body []byte) *vision.Image { 166 | return &vision.Image{ 167 | // Content: Image content, represented as a stream of bytes. 168 | Content: base64.StdEncoding.EncodeToString(body), 169 | } 170 | } 171 | 172 | // NewAnnotateImageSource returns a pointer to a new vision's Image. 173 | // It's contained external image source (i.e. Google Cloud Storage image 174 | // location). 175 | func NewAnnotateImageSource(source string) *vision.Image { 176 | return &vision.Image{ 177 | Source: &vision.ImageSource{ 178 | GcsImageUri: source, 179 | }, 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package pigeon 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | vision "google.golang.org/api/vision/v1" 9 | ) 10 | 11 | func TestClient(t *testing.T) { 12 | os.Clearenv() 13 | 14 | assert := assert.New(t) 15 | 16 | cfg := NewConfig() 17 | client, err := New(cfg) 18 | assert.Nil(client) 19 | assert.Error(err) 20 | 21 | os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "credentials/example.json") 22 | cfg = NewConfig() 23 | client, err = New(cfg) 24 | assert.NotNil(client) 25 | assert.NoError(err) 26 | assert.NotNil(client.service) 27 | assert.NotNil(client.ImagesService()) 28 | } 29 | 30 | func TestNewAnnotateImageRequest(t *testing.T) { 31 | assert := assert.New(t) 32 | 33 | var ( 34 | req *vision.AnnotateImageRequest 35 | err error 36 | ) 37 | const ( 38 | gcsImageURI = "gs://bucket/sample.png" 39 | fp = "assets/lenna.jpg" 40 | imageURI = "https://httpbin.org/image/jpeg" 41 | imageURINoExists = "https://httpbin.org/image/jpeg/none" 42 | ) 43 | features := NewFeature(LabelDetection) 44 | client, err := New(nil) 45 | assert.NoError(err) 46 | 47 | // GCS 48 | req, err = client.NewAnnotateImageRequest(gcsImageURI, features) 49 | assert.NoError(err) 50 | if assert.NotNil(req) { 51 | assert.Equal("", req.Image.Content) 52 | assert.Equal(gcsImageURI, req.Image.Source.GcsImageUri) 53 | } 54 | 55 | // Filepath 56 | req, err = client.NewAnnotateImageRequest(fp, features) 57 | assert.NoError(err) 58 | if assert.NotNil(req) { 59 | assert.NotEqual("", req.Image.Content) 60 | assert.Nil(req.Image.Source) 61 | } 62 | 63 | // Image URI 64 | req, err = client.NewAnnotateImageRequest(imageURI, features) 65 | assert.NoError(err) 66 | if assert.NotNil(req) { 67 | assert.NotEqual("", req.Image.Content) 68 | assert.Nil(req.Image.Source) 69 | } 70 | 71 | req, err = client.NewAnnotateImageRequest(imageURINoExists, features) 72 | assert.Error(err) 73 | assert.Nil(req) 74 | } 75 | -------------------------------------------------------------------------------- /cmd/pigeon/README.md: -------------------------------------------------------------------------------- 1 | # Pigeon 2 | 3 | The `pigeon` command submits images to Google Cloud Vision API. 4 | 5 | ## Prerequisite 6 | 7 | You need to export a service account json file to `GOOGLE_APPLICATION_CREDENTIALS` variable. 8 | 9 | ``` 10 | $ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account.json 11 | ``` 12 | 13 | 14 | ## Installation 15 | 16 | Type the following line to install `pigeon`. 17 | 18 | ```shell 19 | $ go get github.com/kaneshin/pigeon/cmd/pigeon 20 | ``` 21 | 22 | Make sure that `pigeon` was installed correctly: 23 | 24 | ```shell 25 | $ pigeon -h 26 | ``` 27 | 28 | 29 | ## Run 30 | 31 | ``` 32 | $ pigeon assets/lenna.jpg 33 | $ pigeon -face gs://bucket_name/lenna.jpg 34 | ``` 35 | 36 | ## Example 37 | 38 | ![pigeon-cmd](https://raw.githubusercontent.com/kaneshin/pigeon/master/assets/pigeon-cmd.gif) 39 | 40 | 41 | ## License 42 | 43 | [The MIT License (MIT)](http://kaneshin.mit-license.org/) 44 | 45 | 46 | ## Author 47 | 48 | Shintaro Kaneko 49 | -------------------------------------------------------------------------------- /cmd/pigeon/flag.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | vision "google.golang.org/api/vision/v1" 7 | 8 | "github.com/kaneshin/pigeon" 9 | ) 10 | 11 | const ( 12 | defaultDetection = pigeon.LabelDetection 13 | ) 14 | 15 | // Detections type 16 | type Detections struct { 17 | face bool 18 | landmark bool 19 | logo bool 20 | label bool 21 | text bool 22 | safeSearch bool 23 | imageProperties bool 24 | args []string 25 | flag *flag.FlagSet 26 | } 27 | 28 | // DetectionsParse parses the command-line flags from arguments and returns 29 | // a new pointer of a Detections object.. 30 | func DetectionsParse(args []string) *Detections { 31 | f := flag.NewFlagSet("Detections", flag.ExitOnError) 32 | faceDetection := f.Bool("face", false, "This flag specifies the face detection of the feature") 33 | landmarkDetection := f.Bool("landmark", false, "This flag specifies the landmark detection of the feature") 34 | logoDetection := f.Bool("logo", false, "This flag specifies the logo detection of the feature") 35 | labelDetection := f.Bool("label", false, "This flag specifies the label detection of the feature") 36 | textDetection := f.Bool("text", false, "This flag specifies the text detection (OCR) of the feature") 37 | safeSearchDetection := f.Bool("safe-search", false, "This flag specifies the safe-search of the feature") 38 | imageProperties := f.Bool("image-properties", false, "This flag specifies the image safe-search properties of the feature") 39 | f.Usage = func() { 40 | f.PrintDefaults() 41 | } 42 | f.Parse(args) 43 | return &Detections{ 44 | face: *faceDetection, 45 | landmark: *landmarkDetection, 46 | logo: *logoDetection, 47 | label: *labelDetection, 48 | text: *textDetection, 49 | safeSearch: *safeSearchDetection, 50 | imageProperties: *imageProperties, 51 | flag: f, 52 | } 53 | } 54 | 55 | // Args returns the non-flag command-line arguments. 56 | func (d *Detections) Args() []string { 57 | return d.flag.Args() 58 | } 59 | 60 | // Usage prints options of the Detection object. 61 | func (d *Detections) Usage() { 62 | d.flag.Usage() 63 | } 64 | 65 | // Features returns a slice of pointers of vision.Feature. 66 | func (d *Detections) Features() []*vision.Feature { 67 | list := []int{} 68 | if d.face { 69 | list = append(list, pigeon.FaceDetection) 70 | } 71 | if d.landmark { 72 | list = append(list, pigeon.LandmarkDetection) 73 | } 74 | if d.logo { 75 | list = append(list, pigeon.LogoDetection) 76 | } 77 | if d.label { 78 | list = append(list, pigeon.LabelDetection) 79 | } 80 | if d.text { 81 | list = append(list, pigeon.TextDetection) 82 | } 83 | if d.safeSearch { 84 | list = append(list, pigeon.SafeSearchDetection) 85 | } 86 | if d.imageProperties { 87 | list = append(list, pigeon.ImageProperties) 88 | } 89 | 90 | if len(list) == 0 { 91 | list = append(list, defaultDetection) 92 | } 93 | 94 | features := make([]*vision.Feature, len(list)) 95 | for i := 0; i < len(list); i++ { 96 | features[i] = pigeon.NewFeature(list[i]) 97 | } 98 | return features 99 | } 100 | -------------------------------------------------------------------------------- /cmd/pigeon/flag_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDetections(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | var ( 13 | args = []string{} 14 | detects *Detections 15 | ) 16 | 17 | detects = DetectionsParse(args) 18 | assert.EqualValues(0, len(detects.Args())) 19 | f := detects.Features() 20 | if assert.EqualValues(1, len(f)) { 21 | feature := f[0] 22 | assert.EqualValues(0, feature.MaxResults) 23 | assert.Equal("LABEL_DETECTION", feature.Type) 24 | } 25 | assert.False(detects.face) 26 | assert.False(detects.landmark) 27 | assert.False(detects.logo) 28 | assert.False(detects.label) 29 | assert.False(detects.text) 30 | assert.False(detects.safeSearch) 31 | assert.False(detects.imageProperties) 32 | 33 | args = []string{"-landmark", "lenna.jpg"} 34 | detects = DetectionsParse(args) 35 | assert.EqualValues(1, len(detects.Args())) 36 | f = detects.Features() 37 | if assert.Equal(1, len(f)) { 38 | feature := f[0] 39 | assert.EqualValues(0, feature.MaxResults) 40 | assert.Equal("LANDMARK_DETECTION", feature.Type) 41 | } 42 | assert.False(detects.face) 43 | assert.True(detects.landmark) 44 | assert.False(detects.logo) 45 | assert.False(detects.label) 46 | assert.False(detects.text) 47 | assert.False(detects.safeSearch) 48 | assert.False(detects.imageProperties) 49 | 50 | args = []string{"-face", "-landmark", "-logo", "-label", "-text", "-safe-search", "-image-properties", "lenna.jpg"} 51 | detects = DetectionsParse(args) 52 | assert.EqualValues(1, len(detects.Args())) 53 | assert.EqualValues(7, len(detects.Features())) 54 | assert.True(detects.face) 55 | assert.True(detects.landmark) 56 | assert.True(detects.logo) 57 | assert.True(detects.label) 58 | assert.True(detects.text) 59 | assert.True(detects.safeSearch) 60 | assert.True(detects.imageProperties) 61 | } 62 | -------------------------------------------------------------------------------- /cmd/pigeon/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/kaneshin/pigeon" 10 | ) 11 | 12 | func main() { 13 | // Parse arguments to run this function. 14 | detects := DetectionsParse(os.Args[1:]) 15 | 16 | if args := detects.Args(); len(args) == 0 { 17 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 18 | detects.Usage() 19 | os.Exit(1) 20 | } 21 | 22 | // Initialize vision service by a credentials json. 23 | client, err := pigeon.New(nil) 24 | if err != nil { 25 | log.Fatalf("Unable to retrieve vision service: %v\n", err) 26 | } 27 | 28 | // To call multiple image annotation requests. 29 | batch, err := client.NewBatchAnnotateImageRequest(detects.Args(), detects.Features()...) 30 | if err != nil { 31 | log.Fatalf("Unable to retrieve image request: %v\n", err) 32 | } 33 | 34 | // Execute the "vision.images.annotate". 35 | res, err := client.ImagesService().Annotate(batch).Do() 36 | if err != nil { 37 | log.Fatalf("Unable to execute images annotate requests: %v\n", err) 38 | } 39 | 40 | // Marshal annotations from responses 41 | body, err := json.MarshalIndent(res.Responses, "", " ") 42 | if err != nil { 43 | log.Fatalf("Unable to marshal the response: %v\n", err) 44 | } 45 | fmt.Println(string(body)) 46 | } 47 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package pigeon 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/kaneshin/pigeon/credentials" 7 | ) 8 | 9 | type ( 10 | // A Config provides service configuration for service clients. By default, 11 | // all clients will use the {defaults.DefaultConfig} structure. 12 | Config struct { 13 | // The credentials object to use when signing requests. 14 | // Defaults to application credentials file. 15 | Credentials *credentials.Credentials 16 | 17 | // The HTTP client to use when sending requests. 18 | // Defaults to `http.DefaultClient`. 19 | HTTPClient *http.Client 20 | } 21 | ) 22 | 23 | // NewConfig returns a new pointer Config object. 24 | func NewConfig() *Config { 25 | return &Config{} 26 | } 27 | 28 | // WithCredentials sets a config Credentials value returning a Config pointer 29 | // for chaining. 30 | func (c *Config) WithCredentials(creds *credentials.Credentials) *Config { 31 | c.Credentials = creds 32 | return c 33 | } 34 | 35 | // WithHTTPClient sets a config HTTPClient value returning a Config pointer 36 | // for chaining. 37 | func (c *Config) WithHTTPClient(client *http.Client) *Config { 38 | c.HTTPClient = client 39 | return c 40 | } 41 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package pigeon 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/kaneshin/pigeon/credentials" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestConfig(t *testing.T) { 12 | assert := assert.New(t) 13 | 14 | cfg := NewConfig() 15 | assert.NotNil(cfg) 16 | assert.Nil(cfg.Credentials) 17 | assert.Nil(cfg.HTTPClient) 18 | 19 | creds := credentials.NewApplicationCredentials("") 20 | client := http.DefaultClient 21 | cfg.WithCredentials(creds). 22 | WithHTTPClient(client) 23 | assert.NotNil(cfg.Credentials) 24 | assert.NotNil(cfg.HTTPClient) 25 | } 26 | 27 | func Benchmark_Config(b *testing.B) { 28 | var c *Config 29 | 30 | b.Run("NewConfig", func(b *testing.B) { 31 | b.ReportAllocs() 32 | b.ResetTimer() 33 | for n := 0; n < b.N; n++ { 34 | c = NewConfig() 35 | _ = c 36 | } 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /credentials/application_credentials_provider.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | // A ApplicationCredentialsProvider retrieves credentials from the current user's home 10 | // directory, and keeps track if those credentials are expired. 11 | type ApplicationCredentialsProvider struct { 12 | // Path to the shared credentials file. 13 | // 14 | // If empty will look for "GOOGLE_APPLICATION_CREDENTIALS" env variable. If the 15 | // env value is empty will default to current user's home directory. 16 | Filename string 17 | 18 | // retrieved states if the credentials have been successfully retrieved. 19 | retrieved bool 20 | } 21 | 22 | // NewApplicationCredentials returns a pointer to a new Credentials object 23 | // wrapping the file provider. 24 | func NewApplicationCredentials(filename string) *Credentials { 25 | return NewCredentials(&ApplicationCredentialsProvider{ 26 | Filename: filename, 27 | retrieved: false, 28 | }) 29 | } 30 | 31 | // Retrieve reads and extracts the shared credentials from the current 32 | // users home directory. 33 | func (p *ApplicationCredentialsProvider) Retrieve() (Value, error) { 34 | p.retrieved = false 35 | 36 | filename, err := p.filename() 37 | if err != nil { 38 | return Value{}, err 39 | } 40 | 41 | f, err := os.Open(filename) 42 | if err != nil { 43 | return Value{}, err 44 | } 45 | defer f.Close() 46 | 47 | creds := Value{} 48 | if err := json.NewDecoder(f).Decode(&creds); err != nil { 49 | return Value{}, err 50 | } 51 | 52 | p.retrieved = true 53 | return creds, nil 54 | } 55 | 56 | // filename returns the filename to use to read google application credentials. 57 | // Will return an error if the user's home directory path cannot be found. 58 | func (p *ApplicationCredentialsProvider) filename() (string, error) { 59 | if p.Filename == "" { 60 | if p.Filename = os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"); p.Filename == "" { 61 | return "", fmt.Errorf("Unable to read GOOGLE_APPLICATION_CREDENTIALS") 62 | } 63 | } 64 | return p.Filename, nil 65 | } 66 | -------------------------------------------------------------------------------- /credentials/application_credentials_provider_test.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestApplicationCredentials(t *testing.T) { 11 | os.Clearenv() 12 | 13 | c := NewApplicationCredentials("example.json") 14 | creds, err := c.Get() 15 | 16 | assert.NoError(t, err, "Expect no error") 17 | 18 | assert.Equal(t, "service_account", creds.Type) 19 | assert.Equal(t, "project-id", creds.ProjectID) 20 | assert.Equal(t, "some_number", creds.PrivateKeyID) 21 | assert.Equal(t, "-----BEGIN PRIVATE KEY-----\n....=\n-----END PRIVATE KEY-----\n", creds.PrivateKey) 22 | assert.Equal(t, "visionapi@project-id.iam.gserviceaccount.com", creds.ClientEmail) 23 | assert.Equal(t, "...", creds.ClientID) 24 | assert.Equal(t, "https://accounts.google.com/o/oauth2/auth", creds.AuthURI) 25 | assert.Equal(t, "https://accounts.google.com/o/oauth2/token", creds.TokenURI) 26 | assert.Equal(t, "https://www.googleapis.com/oauth2/v1/certs", creds.AuthProviderX509CertURL) 27 | assert.Equal(t, "https://www.googleapis.com/robot/v1/metadata/x509/visionapi%40project-id.iam.gserviceaccount.com", creds.ClientX509CertURL) 28 | 29 | t.Run("Provider", func(t *testing.T) { 30 | os.Clearenv() 31 | 32 | p := ApplicationCredentialsProvider{Filename: "example.json"} 33 | creds, err := p.Retrieve() 34 | 35 | assert.NoError(t, err, "Expect no error") 36 | 37 | assert.Equal(t, "service_account", creds.Type) 38 | assert.Equal(t, "project-id", creds.ProjectID) 39 | assert.Equal(t, "some_number", creds.PrivateKeyID) 40 | assert.Equal(t, "-----BEGIN PRIVATE KEY-----\n....=\n-----END PRIVATE KEY-----\n", creds.PrivateKey) 41 | assert.Equal(t, "visionapi@project-id.iam.gserviceaccount.com", creds.ClientEmail) 42 | assert.Equal(t, "...", creds.ClientID) 43 | assert.Equal(t, "https://accounts.google.com/o/oauth2/auth", creds.AuthURI) 44 | assert.Equal(t, "https://accounts.google.com/o/oauth2/token", creds.TokenURI) 45 | assert.Equal(t, "https://www.googleapis.com/oauth2/v1/certs", creds.AuthProviderX509CertURL) 46 | assert.Equal(t, "https://www.googleapis.com/robot/v1/metadata/x509/visionapi%40project-id.iam.gserviceaccount.com", creds.ClientX509CertURL) 47 | 48 | t.Run("No File Error", func(t *testing.T) { 49 | p := ApplicationCredentialsProvider{Filename: "notexist.json"} 50 | creds, err := p.Retrieve() 51 | 52 | assert.Error(t, err, "Should be error") 53 | assert.Equal(t, creds, Value{}) 54 | }) 55 | 56 | t.Run("Not JSON File Error", func(t *testing.T) { 57 | p := ApplicationCredentialsProvider{Filename: "credentials.go"} 58 | creds, err := p.Retrieve() 59 | 60 | assert.Error(t, err, "Should be error") 61 | assert.Equal(t, creds, Value{}) 62 | }) 63 | }) 64 | 65 | t.Run("ProviderWithGOOGLE_APPLICATION_CREDENTIALS_FILE", func(t *testing.T) { 66 | os.Clearenv() 67 | 68 | p := ApplicationCredentialsProvider{} 69 | creds, err := p.Retrieve() 70 | assert.Error(t, err, "Should be error") 71 | 72 | os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "example.json") 73 | creds, err = p.Retrieve() 74 | 75 | assert.NoError(t, err, "Expect no error") 76 | 77 | assert.Equal(t, "service_account", creds.Type) 78 | assert.Equal(t, "project-id", creds.ProjectID) 79 | assert.Equal(t, "some_number", creds.PrivateKeyID) 80 | assert.Equal(t, "-----BEGIN PRIVATE KEY-----\n....=\n-----END PRIVATE KEY-----\n", creds.PrivateKey) 81 | assert.Equal(t, "visionapi@project-id.iam.gserviceaccount.com", creds.ClientEmail) 82 | assert.Equal(t, "...", creds.ClientID) 83 | assert.Equal(t, "https://accounts.google.com/o/oauth2/auth", creds.AuthURI) 84 | assert.Equal(t, "https://accounts.google.com/o/oauth2/token", creds.TokenURI) 85 | assert.Equal(t, "https://www.googleapis.com/oauth2/v1/certs", creds.AuthProviderX509CertURL) 86 | assert.Equal(t, "https://www.googleapis.com/robot/v1/metadata/x509/visionapi%40project-id.iam.gserviceaccount.com", creds.ClientX509CertURL) 87 | }) 88 | } 89 | 90 | func BenchmarkApplicationCredentialsProvider(b *testing.B) { 91 | os.Clearenv() 92 | 93 | p := ApplicationCredentialsProvider{Filename: "example.json"} 94 | _, err := p.Retrieve() 95 | if err != nil { 96 | b.Fatal(err) 97 | } 98 | 99 | b.ResetTimer() 100 | b.RunParallel(func(pb *testing.PB) { 101 | for pb.Next() { 102 | _, err := p.Retrieve() 103 | if err != nil { 104 | b.Fatal(err) 105 | } 106 | } 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /credentials/credentials.go: -------------------------------------------------------------------------------- 1 | // Package credentials provides credential retrieval and management 2 | package credentials 3 | 4 | import ( 5 | "sync" 6 | ) 7 | 8 | // A Value is the service account credentials value for individual credential fields. 9 | type Value struct { 10 | Type string `json:"type"` 11 | ProjectID string `json:"project_id"` 12 | PrivateKeyID string `json:"private_key_id"` 13 | PrivateKey string `json:"private_key"` 14 | ClientEmail string `json:"client_email"` 15 | ClientID string `json:"client_id"` 16 | AuthURI string `json:"auth_uri"` 17 | TokenURI string `json:"token_uri"` 18 | AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"` 19 | ClientX509CertURL string `json:"client_x509_cert_url"` 20 | } 21 | 22 | // IsValid ... 23 | func (v *Value) IsValid() bool { 24 | return v.ProjectID != "" && 25 | v.PrivateKeyID != "" && 26 | v.PrivateKey != "" && 27 | v.ClientEmail != "" && 28 | v.ClientID != "" 29 | } 30 | 31 | // A Provider is the interface for any component which will provide credentials 32 | // Value. 33 | type Provider interface { 34 | // Refresh returns nil if it successfully retrieved the value. 35 | // Error is returned if the value were not obtainable, or empty. 36 | Retrieve() (Value, error) 37 | } 38 | 39 | // A Credentials provides synchronous safe retrieval of service account 40 | // credentials Value. 41 | type Credentials struct { 42 | creds Value 43 | m sync.Mutex 44 | provider Provider 45 | } 46 | 47 | // NewCredentials returns a pointer to a new Credentials with the provider set. 48 | func NewCredentials(provider Provider) *Credentials { 49 | return &Credentials{ 50 | provider: provider, 51 | } 52 | } 53 | 54 | // Get returns the credentials value, or error if the credentials Value failed 55 | // to be retrieved. 56 | func (c *Credentials) Get() (Value, error) { 57 | c.m.Lock() 58 | defer c.m.Unlock() 59 | return c.provider.Retrieve() 60 | } 61 | -------------------------------------------------------------------------------- /credentials/credentials_test.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type stubProvider struct { 11 | creds Value 12 | err error 13 | } 14 | 15 | func (s *stubProvider) Retrieve() (Value, error) { 16 | return s.creds, s.err 17 | } 18 | 19 | func TestCredentialsGet(t *testing.T) { 20 | c := NewCredentials(&stubProvider{ 21 | creds: Value{ 22 | Type: "service_account", 23 | ProjectID: "project-id", 24 | PrivateKeyID: "some_number", 25 | PrivateKey: "-----BEGIN PRIVATE KEY-----\n....=\n-----END PRIVATE KEY-----\n", 26 | ClientEmail: "visionapi@project-id.iam.gserviceaccount.com", 27 | ClientID: "...", 28 | AuthURI: "https://accounts.google.com/o/oauth2/auth", 29 | TokenURI: "https://accounts.google.com/o/oauth2/token", 30 | AuthProviderX509CertURL: "https://www.googleapis.com/oauth2/v1/certs", 31 | ClientX509CertURL: "https://www.googleapis.com/robot/v1/metadata/x509/visionapi%40project-id.iam.gserviceaccount.com", 32 | }, 33 | }) 34 | 35 | creds, err := c.Get() 36 | 37 | assert.NoError(t, err, "Expected no error") 38 | assert.True(t, creds.IsValid()) 39 | assert.Equal(t, "service_account", creds.Type) 40 | assert.Equal(t, "project-id", creds.ProjectID) 41 | assert.Equal(t, "some_number", creds.PrivateKeyID) 42 | assert.Equal(t, "-----BEGIN PRIVATE KEY-----\n....=\n-----END PRIVATE KEY-----\n", creds.PrivateKey) 43 | assert.Equal(t, "visionapi@project-id.iam.gserviceaccount.com", creds.ClientEmail) 44 | assert.Equal(t, "...", creds.ClientID) 45 | assert.Equal(t, "https://accounts.google.com/o/oauth2/auth", creds.AuthURI) 46 | assert.Equal(t, "https://accounts.google.com/o/oauth2/token", creds.TokenURI) 47 | assert.Equal(t, "https://www.googleapis.com/oauth2/v1/certs", creds.AuthProviderX509CertURL) 48 | assert.Equal(t, "https://www.googleapis.com/robot/v1/metadata/x509/visionapi%40project-id.iam.gserviceaccount.com", creds.ClientX509CertURL) 49 | 50 | t.Run("Error", func(t *testing.T) { 51 | c := NewCredentials(&stubProvider{err: errors.New("provider error")}) 52 | 53 | v, err := c.Get() 54 | assert.Error(t, err) 55 | assert.False(t, v.IsValid()) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /credentials/env_provider.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | -------------------------------------------------------------------------------- /credentials/env_provider_test.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | -------------------------------------------------------------------------------- /credentials/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "project-id", 4 | "private_key_id": "some_number", 5 | "private_key": "-----BEGIN PRIVATE KEY-----\n....=\n-----END PRIVATE KEY-----\n", 6 | "client_email": "visionapi@project-id.iam.gserviceaccount.com", 7 | "client_id": "...", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://accounts.google.com/o/oauth2/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/visionapi%40project-id.iam.gserviceaccount.com" 12 | } 13 | -------------------------------------------------------------------------------- /credentials/static_provider.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // A StaticProvider is a set of credentials which are set pragmatically, 9 | // and will never expire. 10 | type StaticProvider struct { 11 | Value 12 | } 13 | 14 | // NewStaticCredentials returns a pointer to a new Credentials object 15 | // wrapping a static credentials value provider. 16 | func NewStaticCredentials(projectID, privateKeyID, privateKey, clientEmail, clientID string) *Credentials { 17 | _, err := url.ParseQuery(clientEmail) 18 | if err != nil { 19 | return nil 20 | } 21 | return NewCredentials(&StaticProvider{Value: Value{ 22 | Type: "service_account", 23 | ProjectID: projectID, 24 | PrivateKeyID: privateKeyID, 25 | PrivateKey: privateKey, 26 | ClientEmail: clientEmail, 27 | ClientID: clientID, 28 | AuthURI: "https://accounts.google.com/o/oauth2/auth", 29 | TokenURI: "https://accounts.google.com/o/oauth2/token", 30 | AuthProviderX509CertURL: "https://www.googleapis.com/oauth2/v1/certs", 31 | ClientX509CertURL: fmt.Sprintf("https://www.googleapis.com/robot/v1/metadata/x509/%s", url.QueryEscape(clientEmail)), 32 | }}) 33 | 34 | } 35 | 36 | // Retrieve returns the credentials or error if the credentials are invalid. 37 | func (s *StaticProvider) Retrieve() (Value, error) { 38 | if s.IsValid() { 39 | return s.Value, nil 40 | } 41 | return Value{}, fmt.Errorf("static credentials are empty") 42 | } 43 | -------------------------------------------------------------------------------- /credentials/static_provider_test.go: -------------------------------------------------------------------------------- 1 | package credentials 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestStaticCredentials(t *testing.T) { 10 | c := NewStaticCredentials( 11 | "project-id", 12 | "some_number", 13 | "-----BEGIN PRIVATE KEY-----\n....=\n-----END PRIVATE KEY-----\n", 14 | "visionapi@project-id.iam.gserviceaccount.com", 15 | "...", 16 | ) 17 | creds, err := c.Get() 18 | 19 | assert.NoError(t, err, "Expect no error") 20 | assert.Equal(t, "service_account", creds.Type) 21 | assert.Equal(t, "project-id", creds.ProjectID) 22 | assert.Equal(t, "some_number", creds.PrivateKeyID) 23 | assert.Equal(t, "-----BEGIN PRIVATE KEY-----\n....=\n-----END PRIVATE KEY-----\n", creds.PrivateKey) 24 | assert.Equal(t, "visionapi@project-id.iam.gserviceaccount.com", creds.ClientEmail) 25 | assert.Equal(t, "...", creds.ClientID) 26 | assert.Equal(t, "https://accounts.google.com/o/oauth2/auth", creds.AuthURI) 27 | assert.Equal(t, "https://accounts.google.com/o/oauth2/token", creds.TokenURI) 28 | assert.Equal(t, "https://www.googleapis.com/oauth2/v1/certs", creds.AuthProviderX509CertURL) 29 | assert.Equal(t, "https://www.googleapis.com/robot/v1/metadata/x509/visionapi%40project-id.iam.gserviceaccount.com", creds.ClientX509CertURL) 30 | 31 | t.Run("Provider", func(t *testing.T) { 32 | p := StaticProvider{ 33 | Value: Value{ 34 | ProjectID: "project-id", 35 | PrivateKeyID: "some_number", 36 | PrivateKey: "-----BEGIN PRIVATE KEY-----\n....=\n-----END PRIVATE KEY-----\n", 37 | ClientEmail: "visionapi@project-id.iam.gserviceaccount.com", 38 | ClientID: "...", 39 | }, 40 | } 41 | creds, err := p.Retrieve() 42 | 43 | assert.NoError(t, err, "Expect no error") 44 | assert.Equal(t, "project-id", creds.ProjectID) 45 | assert.Equal(t, "some_number", creds.PrivateKeyID) 46 | assert.Equal(t, "-----BEGIN PRIVATE KEY-----\n....=\n-----END PRIVATE KEY-----\n", creds.PrivateKey) 47 | assert.Equal(t, "visionapi@project-id.iam.gserviceaccount.com", creds.ClientEmail) 48 | assert.Equal(t, "...", creds.ClientID) 49 | 50 | t.Run("Invalie Error", func(t *testing.T) { 51 | p := StaticProvider{ 52 | Value: Value{ 53 | ProjectID: "", 54 | PrivateKeyID: "", 55 | PrivateKey: "", 56 | ClientEmail: "", 57 | ClientID: "", 58 | }, 59 | } 60 | creds, err := p.Retrieve() 61 | 62 | assert.Error(t, err, "Should be error") 63 | assert.Equal(t, creds, Value{}) 64 | }) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /feature.go: -------------------------------------------------------------------------------- 1 | package pigeon 2 | 3 | import ( 4 | vision "google.golang.org/api/vision/v1" 5 | ) 6 | 7 | const ( 8 | // TypeUnspecified - Unspecified feature type. 9 | TypeUnspecified = iota 10 | // FaceDetection - Run face detection. 11 | FaceDetection 12 | // LandmarkDetection - Run landmark detection. 13 | LandmarkDetection 14 | // LogoDetection - Run logo detection. 15 | LogoDetection 16 | // LabelDetection - Run label detection. 17 | LabelDetection 18 | // TextDetection - Run OCR with big text 19 | TextDetection 20 | // DocumentTextDetection - Run OCR on document 21 | DocumentTextDetection 22 | // SafeSearchDetection - Run various computer vision models to 23 | SafeSearchDetection 24 | // ImageProperties - compute image safe-search properties. 25 | ImageProperties 26 | ) 27 | 28 | // DetectionType returns a value of detection type. 29 | func DetectionType(d int) string { 30 | switch d { 31 | case TypeUnspecified: 32 | return "TYPE_UNSPECIFIED" 33 | case FaceDetection: 34 | return "FACE_DETECTION" 35 | case LandmarkDetection: 36 | return "LANDMARK_DETECTION" 37 | case LogoDetection: 38 | return "LOGO_DETECTION" 39 | case LabelDetection: 40 | return "LABEL_DETECTION" 41 | case TextDetection: 42 | return "TEXT_DETECTION" 43 | case DocumentTextDetection: 44 | return "DOCUMENT_TEXT_DETECTION" 45 | case SafeSearchDetection: 46 | return "SAFE_SEARCH_DETECTION" 47 | case ImageProperties: 48 | return "IMAGE_PROPERTIES" 49 | } 50 | return "" 51 | } 52 | 53 | // NewFeature returns a pointer to a new vision's Feature object. 54 | func NewFeature(d int) *vision.Feature { 55 | return &vision.Feature{Type: DetectionType(d)} 56 | } 57 | -------------------------------------------------------------------------------- /feature_test.go: -------------------------------------------------------------------------------- 1 | package pigeon 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDetectionType(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | assert.Equal("TYPE_UNSPECIFIED", DetectionType(TypeUnspecified)) 13 | assert.Equal("FACE_DETECTION", DetectionType(FaceDetection)) 14 | assert.Equal("LANDMARK_DETECTION", DetectionType(LandmarkDetection)) 15 | assert.Equal("LOGO_DETECTION", DetectionType(LogoDetection)) 16 | assert.Equal("LABEL_DETECTION", DetectionType(LabelDetection)) 17 | assert.Equal("TEXT_DETECTION", DetectionType(TextDetection)) 18 | assert.Equal("SAFE_SEARCH_DETECTION", DetectionType(SafeSearchDetection)) 19 | assert.Equal("IMAGE_PROPERTIES", DetectionType(ImageProperties)) 20 | assert.Equal("", DetectionType(-1)) 21 | 22 | f := NewFeature(LabelDetection) 23 | assert.EqualValues(0, f.MaxResults) 24 | assert.EqualValues("LABEL_DETECTION", f.Type) 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kaneshin/pigeon 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/golang/protobuf v1.3.2 // indirect 7 | github.com/mattn/go-colorable v0.1.4 // indirect 8 | github.com/mattn/go-isatty v0.0.11 // indirect 9 | github.com/mitchellh/gox v1.0.1 // indirect 10 | github.com/stretchr/testify v1.4.0 11 | github.com/tcnksm/ghr v0.13.0 // indirect 12 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect 13 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 14 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect 15 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 // indirect 16 | google.golang.org/api v0.15.0 17 | google.golang.org/appengine v1.6.5 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= 4 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/Songmu/retry v0.1.0 h1:hPA5xybQsksLR/ry/+t/7cFajPW+dqjmjhzZhioBILA= 7 | github.com/Songmu/retry v0.1.0/go.mod h1:7sXIW7eseB9fq0FUvigRcQMVLR9tuHI0Scok+rkpAuA= 8 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 9 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 12 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 13 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 14 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 15 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 16 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 17 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 18 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 19 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 20 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 21 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 22 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 23 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 24 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 25 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= 26 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 27 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 28 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 29 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 30 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 31 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 32 | github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= 33 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 34 | github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 35 | github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= 36 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 37 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 38 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 39 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 40 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 41 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 42 | github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= 43 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 44 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 45 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 46 | github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= 47 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 48 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 49 | github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= 50 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 51 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= 52 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 53 | github.com/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI= 54 | github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4= 55 | github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= 56 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 57 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 58 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 59 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 60 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 61 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 62 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 63 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 64 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 65 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 66 | github.com/tcnksm/ghr v0.13.0 h1:a5ZbaUAfiaiw6rEDJVUEDYA9YreZOkh3XAfXHWn8zu8= 67 | github.com/tcnksm/ghr v0.13.0/go.mod h1:tcp6tzbRYE0LqFSG7ykXP/BVG1/2BkX6aIn9FFV1mIQ= 68 | github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw= 69 | github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE= 70 | github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2oeKxpIUmtiDV5sn71VgeQgg6vcE7k= 71 | github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM= 72 | go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= 73 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 74 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 75 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 76 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 77 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 78 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 79 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 80 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 81 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 82 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 83 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 84 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 85 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 86 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 87 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= 88 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 89 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 90 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= 91 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 92 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 93 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 94 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 95 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 96 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= 97 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 98 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 99 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 100 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 101 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 102 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 103 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 105 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 106 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 107 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 108 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 109 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 110 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= 111 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM= 114 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 116 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 117 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 118 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 119 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 120 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 121 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 122 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 123 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 124 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 125 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 126 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 127 | google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA= 128 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 129 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 130 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 131 | google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= 132 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 133 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 134 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 135 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 136 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 137 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 138 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= 139 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 140 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 141 | google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= 142 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 143 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 144 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 145 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 146 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 147 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 148 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 149 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 150 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 151 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 152 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 153 | -------------------------------------------------------------------------------- /pigeon.go: -------------------------------------------------------------------------------- 1 | package pigeon 2 | 3 | // Version represents pigeon's semantic version. 4 | const Version = "v1.1.1" 5 | --------------------------------------------------------------------------------