scnnr 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 701520f0f2410d7300d04bac498aec68db34597f
4
- data.tar.gz: 15d6e7b77a2ad5c91d96fa302d4eb1fc800792f3
2
+ SHA256:
3
+ metadata.gz: 5cb9874e1854429b787e165fbff198da0e2992ae506a6a84f0c85f42199701d0
4
+ data.tar.gz: 8c59dba0786ab878aef02294606f10fde6eb06fb154ea39ad8473eb8b498e02a
5
5
  SHA512:
6
- metadata.gz: 99379cccbdb705692acd048f32355638b4084c19071c7c2833604fb1356164a36982aab1e7aa505d66495650524faf1d88d492e2eb854308b38b052ed821d0c0
7
- data.tar.gz: ab1d3a36414710519e793259a1a9f655da7c6333971ac145e834bbb62fe02257985092cfd54568c78dd6aa86b4947e5961c9270adf44618adae7a121dba6c62b
6
+ metadata.gz: 77805ba9b1a35a675e918222e2925a56195cfbf10366ca6fe07d551d1f7ac95f8170acf7facd9d02e4e7c9b66aa3c1f3959f7d9b37c502b087b7d9c7a815d767
7
+ data.tar.gz: 0de1c9361be51d20fe0f95e57c9277efdac54450dceca1e065700bd07fa263db2635279438f65acb734a615b33b9cf083bfac18ef9dfb57c12891d06657db401
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # Check https://circleci.com/docs/2.0/language-ruby/ for more details
4
4
  #
5
- version: 2
5
+ version: 2.1
6
6
  jobs:
7
7
  build: &build
8
8
  docker:
@@ -13,13 +13,15 @@ jobs:
13
13
  - run:
14
14
  name: install dependencies
15
15
  command: |
16
- bundle install --jobs=4 --retry=3 --path vendor/bundle
16
+ gem install bundler -v 2.1.2
17
+ bundle config set path vendor/bundle
18
+ bundle install --jobs=4 --retry=3
17
19
  # run tests!
18
20
  - run:
19
21
  name: run tests
20
22
  command: |
21
23
  mkdir /tmp/test-results
22
- bundle exec rubocop --format progress -r $(bundle show rubocop-junit-formatter)/lib/rubocop/formatter/junit_formatter.rb --format RuboCop::Formatter::JUnitFormatter --out /tmp/test-results/rubocop.xml
24
+ bundle exec rubocop --format progress --format junit --out /tmp/test-results/rubocop.xml
23
25
  bundle exec rspec --format progress \
24
26
  --format RspecJunitFormatter \
25
27
  --out /tmp/test-results/rspec.xml \
@@ -30,17 +32,27 @@ jobs:
30
32
  - store_artifacts:
31
33
  path: /tmp/test-results
32
34
  destination: test-results
33
- ruby-2.3.5:
35
+ ruby-24:
34
36
  <<: *build
35
37
  docker:
36
- - image: circleci/ruby:2.3.5-node-browsers
37
- ruby-2.4.1:
38
+ - image: circleci/ruby:2.4
39
+ ruby-25:
38
40
  <<: *build
39
41
  docker:
40
- - image: circleci/ruby:2.4.1-node-browsers
42
+ - image: circleci/ruby:2.5
43
+ ruby-26:
44
+ <<: *build
45
+ docker:
46
+ - image: circleci/ruby:2.6
47
+ ruby-27:
48
+ <<: *build
49
+ docker:
50
+ - image: circleci/ruby:2.7
41
51
  workflows:
42
52
  version: 2
43
53
  ruby-multi-build:
44
54
  jobs:
45
- - ruby-2.3.5
46
- - ruby-2.4.1
55
+ - ruby-24
56
+ - ruby-25
57
+ - ruby-26
58
+ - ruby-27
@@ -4,9 +4,6 @@ require:
4
4
  AllCops:
5
5
  TargetRubyVersion: 2.4
6
6
 
7
- Metrics/LineLength:
8
- Max: 120
9
-
10
7
  Metrics/ClassLength:
11
8
  Max: 300
12
9
 
@@ -29,7 +26,10 @@ Style/CollectionMethods:
29
26
  Style/TrailingCommaInArguments:
30
27
  EnforcedStyleForMultiline: no_comma
31
28
 
32
- Style/TrailingCommaInLiteral:
29
+ Style/TrailingCommaInArrayLiteral:
30
+ EnforcedStyleForMultiline: comma
31
+
32
+ Style/TrailingCommaInHashLiteral:
33
33
  EnforcedStyleForMultiline: comma
34
34
 
35
35
  Style/Documentation:
@@ -38,15 +38,15 @@ Style/Documentation:
38
38
  Style/RedundantSelf:
39
39
  Enabled: false
40
40
 
41
+ Layout/LineLength:
42
+ Max: 120
43
+
41
44
  Layout/MultilineMethodCallIndentation:
42
45
  EnforcedStyle: indented
43
46
 
44
- Layout/IndentHash:
47
+ Layout/FirstHashElementIndentation:
45
48
  EnforcedStyle: consistent
46
49
 
47
- Style/BracesAroundHashParameters:
48
- Enabled: false
49
-
50
50
  RSpec/NestedGroups:
51
51
  Max: 5
52
52
 
@@ -65,3 +65,148 @@ RSpec/MessageSpies:
65
65
  RSpec/AnyInstance:
66
66
  Exclude:
67
67
  - 'spec/scnnr/connection_spec.rb'
68
+
69
+ RSpec/ContextWording:
70
+ Prefixes:
71
+ - when
72
+ - with
73
+ - without
74
+ - and
75
+
76
+ RSpec/MultipleMemoizedHelpers:
77
+ Max: 7
78
+
79
+ Layout/EmptyLinesAroundAttributeAccessor: # (new in 0.83)
80
+ Enabled: true
81
+
82
+ Layout/SpaceAroundMethodCallOperator: # (new in 0.82)
83
+ Enabled: true
84
+
85
+ Lint/BinaryOperatorWithIdenticalOperands: # (new in 0.89)
86
+ Enabled: true
87
+
88
+ Lint/DeprecatedOpenSSLConstant: # (new in 0.84)
89
+ Enabled: true
90
+
91
+ Lint/DuplicateElsifCondition: # (new in 0.88)
92
+ Enabled: true
93
+
94
+ Lint/DuplicateRequire: # (new in 0.90)
95
+ Enabled: true
96
+
97
+ Lint/DuplicateRescueException: # (new in 0.89)
98
+ Enabled: true
99
+
100
+ Lint/EmptyConditionalBody: # (new in 0.89)
101
+ Enabled: true
102
+
103
+ Lint/EmptyFile: # (new in 0.90)
104
+ Enabled: true
105
+
106
+ Lint/FloatComparison: # (new in 0.89)
107
+ Enabled: true
108
+
109
+ Lint/MissingSuper: # (new in 0.89)
110
+ Enabled: true
111
+
112
+ Lint/MixedRegexpCaptureTypes: # (new in 0.85)
113
+ Enabled: true
114
+
115
+ Lint/OutOfRangeRegexpRef: # (new in 0.89)
116
+ Enabled: true
117
+
118
+ Lint/RaiseException: # (new in 0.81)
119
+ Enabled: true
120
+
121
+ Lint/SelfAssignment: # (new in 0.89)
122
+ Enabled: true
123
+
124
+ Lint/StructNewOverride: # (new in 0.81)
125
+ Enabled: true
126
+
127
+ Lint/TopLevelReturnWithArgument: # (new in 0.89)
128
+ Enabled: true
129
+
130
+ Lint/TrailingCommaInAttributeDeclaration: # (new in 0.90)
131
+ Enabled: true
132
+
133
+ Lint/UnreachableLoop: # (new in 0.89)
134
+ Enabled: true
135
+
136
+ Lint/UselessMethodDefinition: # (new in 0.90)
137
+ Enabled: true
138
+
139
+ Style/AccessorGrouping: # (new in 0.87)
140
+ Enabled: true
141
+
142
+ Style/ArrayCoercion: # (new in 0.88)
143
+ Enabled: true
144
+
145
+ Style/BisectedAttrAccessor: # (new in 0.87)
146
+ Enabled: true
147
+
148
+ Style/CaseLikeIf: # (new in 0.88)
149
+ Enabled: true
150
+
151
+ Style/CombinableLoops: # (new in 0.90)
152
+ Enabled: true
153
+
154
+ Style/ExplicitBlockArgument: # (new in 0.89)
155
+ Enabled: true
156
+
157
+ Style/ExponentialNotation: # (new in 0.82)
158
+ Enabled: true
159
+
160
+ Style/GlobalStdStream: # (new in 0.89)
161
+ Enabled: true
162
+
163
+ Style/HashAsLastArrayItem: # (new in 0.88)
164
+ Enabled: true
165
+
166
+ Style/HashEachMethods: # (new in 0.80)
167
+ Enabled: true
168
+
169
+ Style/HashLikeCase: # (new in 0.88)
170
+ Enabled: true
171
+
172
+ Style/HashTransformKeys: # (new in 0.80)
173
+ Enabled: true
174
+
175
+ Style/HashTransformValues: # (new in 0.80)
176
+ Enabled: true
177
+
178
+ Style/KeywordParametersOrder: # (new in 0.90)
179
+ Enabled: true
180
+
181
+ Style/OptionalBooleanParameter: # (new in 0.89)
182
+ Enabled: true
183
+
184
+ Style/RedundantAssignment: # (new in 0.87)
185
+ Enabled: true
186
+
187
+ Style/RedundantFetchBlock: # (new in 0.86)
188
+ Enabled: true
189
+
190
+ Style/RedundantFileExtensionInRequire: # (new in 0.88)
191
+ Enabled: true
192
+
193
+ Style/RedundantRegexpCharacterClass: # (new in 0.85)
194
+ Enabled: true
195
+
196
+ Style/RedundantRegexpEscape: # (new in 0.85)
197
+ Enabled: true
198
+
199
+ Style/RedundantSelfAssignment: # (new in 0.90)
200
+ Enabled: true
201
+
202
+ Style/SingleArgumentDig: # (new in 0.89)
203
+ Enabled: true
204
+
205
+ Style/SlicingWithRange: # (new in 0.83)
206
+ Enabled: true
207
+
208
+ Style/SoleNestedConditional: # (new in 0.89)
209
+ Enabled: true
210
+
211
+ Style/StringConcatenation: # (new in 0.89)
212
+ Enabled: true
@@ -4,13 +4,39 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.0.0] - 2020-09-14
8
+ ### Added
9
+ - Start supporting Ruby 2.7.
10
+ - Provide image error details.
11
+ - Raise `UnexpectedError` for unknown recognition error types.
12
+
13
+ ### Removed
14
+ - Support for Ruby 2.3.
15
+
16
+ ## [1.3.0] - 2019-08-30
17
+ ### Added
18
+ - Support a retrying feature on `Scnnr::Connection`.
19
+ - Start supporting Ruby 2.5 and 2.6.
20
+
21
+ ## [1.2.0] - 2018-10-11
22
+ ### Added
23
+ - Support `target` parameter for `Scnnr::Client#coordinate`.
24
+
25
+ ## [1.1.1] - 2018-06-20
26
+ ### Added
27
+ - Support `force` parameter for `Scnnr::Client#recognize_url`.
28
+
29
+ ## [1.1.0] - 2018-03-23
30
+ ### Added
31
+ - Support `public` parameter for `Scnnr::Client#recognize_image`.
32
+ - Add `Scnnr::Client#coordinate` to request `POST /v1/coordinates`.
7
33
 
8
34
  ## [1.0.0] - 2017-11-01
9
35
  ### Added
10
36
  - Allow timeouts greater than 25s when recognising images.
11
37
 
12
38
  ### Fixed
13
- - Fix that `Scnnr::Client#fetch` with a long timeout does not make multiple request.
39
+ - Fix that `Scnnr::Client#fetch` with a long timeout does not make multiple requests.
14
40
 
15
41
  ### Removed
16
42
  - Remove `async` from `Scnnr::Response`.
data/Gemfile CHANGED
@@ -4,18 +4,23 @@ source 'https://rubygems.org'
4
4
 
5
5
  git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
6
 
7
- gem 'rake', '~> 10.0'
7
+ gem 'rake', '~> 13.0'
8
8
 
9
9
  group :development do
10
10
  gem 'pry', '~> 0.10'
11
+
12
+ gem 'guard'
13
+ gem 'guard-rspec'
14
+ gem 'guard-rubocop'
11
15
  end
12
16
 
13
17
  group :test do
14
18
  gem 'rspec', '~> 3.5'
15
19
  gem 'rspec_junit_formatter', '~> 0.3'
16
- gem 'rubocop', '~> 0.49.0'
17
- gem 'rubocop-junit-formatter', '~> 0.1'
18
- gem 'rubocop-rspec', '~> 1.15.0'
20
+
21
+ gem 'rubocop', '~> 0.90.0'
22
+ gem 'rubocop-rspec', '~> 1.43.2'
23
+
19
24
  gem 'webmock', '~> 3.0'
20
25
  end
21
26
 
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # More info at https://github.com/guard/guard#readme
4
+
5
+ guard :rspec, cmd: 'bundle exec rspec' do
6
+ require 'guard/rspec/dsl'
7
+ dsl = Guard::RSpec::Dsl.new(self)
8
+
9
+ # Feel free to open issues for suggestions and improvements
10
+
11
+ # RSpec files
12
+ rspec = dsl.rspec
13
+ watch(rspec.spec_helper) { rspec.spec_dir }
14
+ watch(rspec.spec_support) { rspec.spec_dir }
15
+ watch(rspec.spec_files)
16
+
17
+ # Ruby files
18
+ ruby = dsl.ruby
19
+ dsl.watch_spec_files_for(ruby.lib_files)
20
+ end
21
+
22
+ guard :rubocop do
23
+ watch(/.+\.rb$/)
24
+ watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
25
+ end
data/README.md CHANGED
@@ -1,20 +1,28 @@
1
1
  # Official #CBK scnnr client library for Ruby.
2
+ [![CircleCI](https://circleci.com/gh/NEWROPE/scnnr-ruby.svg?style=svg)](https://circleci.com/gh/NEWROPE/scnnr-ruby)
2
3
 
3
4
  - [#CBK scnnr](https://scnnr.cubki.jp/)
4
5
  - [API Documentation](https://api.scnnr.cubki.jp/v1/docs)
5
6
 
7
+ ## Supported Ruby versions
8
+ - Ruby 2.4+
9
+
6
10
  ## Installation
11
+
7
12
  ### Bundler
13
+
8
14
  ```
9
15
  gem 'scnnr'
10
16
  ```
11
17
 
12
18
  ### Manual
19
+
13
20
  ```
14
21
  gem install scnnr
15
22
  ```
16
23
 
17
24
  ## Configuration
25
+
18
26
  You can pass configuration options as a block to `Scnnr::Client.new`.
19
27
 
20
28
  ```
@@ -28,22 +36,33 @@ end
28
36
  ```
29
37
 
30
38
  ## Examples
39
+
31
40
  ### Basic usage
41
+
42
+ #### Recognize images
43
+
32
44
  Request image recognition by an image URL.
33
45
 
46
+ Refer: [`POST /v1/remote/recognitions`](https://api.scnnr.cubki.jp/v1/docs#tag/remoterecognitions%2Fpaths%2F~1remote~1recognitions%2Fpost)
47
+
34
48
  ```
35
49
  url = 'https://example.com/dummy.jpg'
36
50
  recognition = client.recognize_url(url)
37
51
 
38
- # you can override config.timeout.
52
+ # You can override config.timeout.
39
53
  recognition = client.recognize_url(url, timeout: 10)
40
54
  ```
41
55
 
42
56
  Request image recognition by a binary image.
43
57
 
58
+ Refer: [`POST /v1/recognitions`](https://api.scnnr.cubki.jp/v1/docs#tag/recognitions%2Fpaths%2F~1recognitions%2Fpost)
59
+
44
60
  ```
45
61
  img = File.open('dummy_image_file', 'rb')
46
62
  recognition = client.recognize_image(img)
63
+
64
+ # You can use public parameter as well.
65
+ recognition = client.recognize_image(img, timeout: 10, public: true)
47
66
  ```
48
67
 
49
68
  `Recognition` class represents the image recognition result from API.
@@ -81,12 +100,47 @@ recognition.to_h
81
100
  "category"=>"dress",
82
101
  "labels"=>[{"name"=>"グリーン", "score"=>0.9765959}, {"name"=>"ワンピース", "score"=>0.94697183}, {"name"=>"カーキ", "score"=>0.8136864}, {"name"=>"無地", "score"=>0.54719794}, {"name"=>"フレア", "score"=>0.51572186}]}],
83
102
  "state"=>"finished"}
103
+
104
+ # If you recognize an image with `public` parameter, the `recognition` has `image`.
105
+ recognition.to_h
106
+ => {"id"=>"20170829/ed4c674c-7970-4e9c-9b26-1b6076b36b49",
107
+ "image"=>
108
+ {"url"=>"some-image-url",
109
+ "size"=>{"height"=>400, "width"=>400}},
110
+ "objects"=>
111
+ [{"bounding_box"=>{"bottom"=>0.2696995, "left"=>0.3842466, "right"=>0.57190025, "top"=>0.14457992},
112
+ "category"=>"hat",
113
+ "labels"=>
114
+ [{"name"=>"ハット", "score"=>0.9985399},
115
+ {"name"=>"中折れ", "score"=>0.99334323},
116
+ {"name"=>"ストローハット", "score"=>0.95629793},
117
+ {"name"=>"ベージュ", "score"=>0.9062561},
118
+ {"name"=>"つば広ハット", "score"=>0.7737022},
119
+ {"name"=>"ホワイト", "score"=>0.5695046}]},
120
+ {"bounding_box"=>{"bottom"=>0.95560884, "left"=>0.41641566, "right"=>0.5212327, "top"=>0.8452401},
121
+ "category"=>"shoe",
122
+ "labels"=>
123
+ [{"name"=>"サンダル", "score"=>0.93934095},
124
+ {"name"=>"ホワイト", "score"=>0.74320596},
125
+ {"name"=>"パンプス", "score"=>0.70763165},
126
+ {"name"=>"サボ", "score"=>0.69153166},
127
+ {"name"=>"ストラップ", "score"=>0.66519636},
128
+ {"name"=>"ウェッジソール", "score"=>0.6325865},
129
+ {"name"=>"オープントゥ", "score"=>0.61965847},
130
+ {"name"=>"アンクルストラップ", "score"=>0.576824},
131
+ {"name"=>"厚底", "score"=>0.53842664}]},
132
+ {"bounding_box"=>{"bottom"=>0.7018228, "left"=>0.35182703, "right"=>0.6113004, "top"=>0.25296396},
133
+ "category"=>"dress",
134
+ "labels"=>[{"name"=>"グリーン", "score"=>0.9765959}, {"name"=>"ワンピース", "score"=>0.94697183}, {"name"=>"カーキ", "score"=>0.8136864}, {"name"=>"無地", "score"=>0.54719794}, {"name"=>"フレア", "score"=>0.51572186}]}],
135
+ "state"=>"finished"}
84
136
  ```
85
137
 
86
138
  If the timeout value is zero or `nil`, you will get `Recognition` instance whose state is `queued`.
87
139
 
88
140
  Then you can fetch the recognition result using `Scnnr::Client#fetch`.
89
141
 
142
+ Refer: [`GET /v1/recognitions/*`](https://api.scnnr.cubki.jp/v1/docs#tag/recognitions%2Fpaths%2F~1recognitions~1*%2Fget)
143
+
90
144
  ```
91
145
  recognition.queued?
92
146
  => true
@@ -99,6 +153,32 @@ recognition.finished?
99
153
  => true
100
154
  ```
101
155
 
156
+ #### Generate fashion coordinates
157
+
158
+ Request fashion coordinates generation.
159
+
160
+ Refer: [`POST /v1/coordinates`](https://api.scnnr.cubki.jp/v1/docs#tag/coordinates%2Fpaths%2F~1coordinates%2Fpost)
161
+
162
+ ```
163
+ tastes = { casual: 0.7, girly: 0.3 }
164
+ coordinate = client.coordinate('tops', ['グレー', 'パーカー'], tastes, target: 3)
165
+
166
+ coordinate.to_h
167
+ => {"items"=>
168
+ [{"category"=>"tops", "labels"=>[{"name"=>"グレー", "score"=>nil}, {"name"=>"パーカー", "score"=>nil}]},
169
+ {"category"=>"shoe",
170
+ "labels"=>
171
+ [{"name"=>"ネイビー", "score"=>nil},
172
+ {"name"=>"スニーカー", "score"=>nil},
173
+ {"name"=>"ランニング", "score"=>nil}]},
174
+ {"category"=>"dress",
175
+ "labels"=>
176
+ [{"name"=>"デニム", "score"=>nil},
177
+ {"name"=>"ブルー", "score"=>nil},
178
+ {"name"=>"サロペット", "score"=>nil},
179
+ {"name"=>"オーバーオール", "score"=>nil}]}]}
180
+ ```
181
+
102
182
  ### Error handling
103
183
 
104
184
  If the recognition processing is not completed within the timeout time or the recognition failed,
@@ -2,12 +2,19 @@
2
2
 
3
3
  require 'scnnr/configuration'
4
4
  require 'scnnr/errors'
5
+ require 'scnnr/errors/image'
6
+ require 'scnnr/errors/image/response'
7
+ require 'scnnr/label'
5
8
  require 'scnnr/recognition'
9
+ require 'scnnr/recognition/image'
6
10
  require 'scnnr/recognition/object'
7
11
  require 'scnnr/recognition/object/bounding_box'
8
12
  require 'scnnr/recognition/object/label'
13
+ require 'scnnr/coordinate'
14
+ require 'scnnr/coordinate/item'
9
15
  require 'scnnr/client'
10
16
  require 'scnnr/connection'
17
+ require 'scnnr/routing'
11
18
  require 'scnnr/polling_manager'
12
19
  require 'scnnr/response'
13
20
  require 'scnnr/version'
@@ -5,7 +5,11 @@ module Scnnr
5
5
  require 'net/http'
6
6
  require 'json'
7
7
 
8
- ENDPOINT_BASE = 'https://api.scnnr.cubki.jp'
8
+ TASTES = %i[
9
+ boyish casual celebrity conservative
10
+ feminine girly gyaru harajuku
11
+ mode natural_style
12
+ ].freeze
9
13
 
10
14
  def initialize
11
15
  yield(self.config) if block_given?
@@ -16,36 +20,52 @@ module Scnnr
16
20
  end
17
21
 
18
22
  def recognize_image(image, options = {})
19
- PollingManager.start(self, merge_options(options)) do |opts|
20
- uri = construct_uri('recognitions', opts)
23
+ options = merge_options options
24
+ PollingManager.start(self, options) do |opts|
25
+ uri = construct_uri('recognitions', %i[timeout public], opts)
21
26
  response = post_connection(uri, opts).send_stream(image)
22
- handle_response(response)
27
+ Response.new(response).build_recognition
23
28
  end
24
29
  end
25
30
 
26
31
  def recognize_url(url, options = {})
27
- PollingManager.start(self, merge_options(options)) do |opts|
28
- uri = construct_uri('remote/recognitions', opts)
32
+ options = merge_options options
33
+ PollingManager.start(self, options) do |opts|
34
+ uri = construct_uri('remote/recognitions', %i[timeout force], opts)
29
35
  response = post_connection(uri, opts).send_json({ url: url })
30
- handle_response(response)
36
+ Response.new(response).build_recognition
31
37
  end
32
38
  end
33
39
 
34
40
  def fetch(recognition_id, options = {})
41
+ options = merge_options options
35
42
  return request(recognition_id, options) if options.delete(:polling) == false
36
- options = merge_options(options)
43
+
37
44
  PollingManager.new(options.delete(:timeout)).polling(self, recognition_id, options)
38
45
  end
39
46
 
47
+ def coordinate(category, labels, taste = {}, options = {})
48
+ options = merge_options options
49
+ uri = construct_uri('coordinates', %i[target], options)
50
+ payload = {
51
+ item: { category: category, labels: labels },
52
+ taste: TASTES.each_with_object({}) { |key, memo| memo[key] = taste[key] if taste[key] },
53
+ }
54
+ response = post_connection(uri, options).send_json(payload)
55
+ Response.new(response).build_coordinate
56
+ end
57
+
40
58
  private
41
59
 
42
60
  def merge_options(options = {})
43
61
  self.config.to_h.merge(options)
44
62
  end
45
63
 
46
- def construct_uri(path, options = {})
47
- options = merge_options(options)
48
- URI.parse("#{ENDPOINT_BASE}/#{options[:api_version]}/#{path}?timeout=#{options[:timeout]}")
64
+ def construct_uri(path, allowed_params, options = {})
65
+ Routing.new(
66
+ path, options[:api_version],
67
+ options, allowed_params
68
+ ).to_url
49
69
  end
50
70
 
51
71
  def get_connection(uri, options = {})
@@ -57,15 +77,9 @@ module Scnnr
57
77
  end
58
78
 
59
79
  def request(recognition_id, options = {})
60
- options = merge_options(options)
61
- uri = construct_uri("recognitions/#{recognition_id}", options)
80
+ uri = construct_uri("recognitions/#{recognition_id}", %i[timeout], options)
62
81
  response = get_connection(uri, options).send_request
63
- handle_response(response)
64
- end
65
-
66
- def handle_response(response)
67
- response = Response.new(response)
68
- response.build_recognition
82
+ Response.new(response).build_recognition
69
83
  end
70
84
  end
71
85
  end
@@ -4,7 +4,7 @@ module Scnnr
4
4
  Configuration = Struct.new(:api_key, :api_version, :timeout, :logger) do
5
5
  require 'logger'
6
6
 
7
- DEFAULT_LOGGER = Logger.new(STDOUT, level: :info)
7
+ DEFAULT_LOGGER = Logger.new($stdout, level: :info)
8
8
 
9
9
  def initialize
10
10
  super(nil, 'v1', 0, DEFAULT_LOGGER)
@@ -5,6 +5,15 @@ module Scnnr
5
5
  require 'net/http'
6
6
  require 'json'
7
7
 
8
+ RETRY_LIMIT = 3
9
+ RETRY_SLEEP_TIME = 1
10
+ RETRY_ERROR_CLASSES = [
11
+ Timeout::Error, Errno::EINVAL,
12
+ Errno::ECONNRESET, EOFError,
13
+ Net::HTTPBadResponse, Net::ProtocolError,
14
+ Net::HTTPHeaderSyntaxError
15
+ ].freeze
16
+
8
17
  def initialize(uri, method, api_key, logger)
9
18
  @uri = uri
10
19
  @method = method
@@ -28,19 +37,32 @@ module Scnnr
28
37
  end
29
38
  end
30
39
 
31
- def send_request
32
- block = block_given? ? Proc.new : nil
33
- request = build_request(&block)
34
- run_request(request)
40
+ def send_request(&block)
41
+ request = block_given? ? build_request(&block) : build_request
42
+ with_retries do
43
+ Net::HTTP.start(@uri.host, @uri.port, use_ssl: use_ssl?) do |http|
44
+ @logger&.info("Started #{@method.upcase} #{@uri}")
45
+ http.request(request)
46
+ end
47
+ end
35
48
  end
36
49
 
37
50
  private
38
51
 
39
- def run_request(request)
40
- Net::HTTP.start(@uri.host, @uri.port, use_ssl: use_ssl?) do |http|
41
- @logger&.info("Started #{@method.upcase} #{@uri}")
42
- http.request(request)
52
+ def with_retries
53
+ yield
54
+ rescue *RETRY_ERROR_CLASSES => e
55
+ retry_count ||= 0
56
+
57
+ if retry_count < RETRY_LIMIT
58
+ retry_count += 1
59
+ @logger&.info("Retrying to connect: #{@uri}, attempt: #{retry_count}")
60
+
61
+ sleep RETRY_SLEEP_TIME
62
+ retry
43
63
  end
64
+
65
+ raise e.class, "#{e.message} (Endpoint: #{@method.upcase} #{@uri})"
44
66
  end
45
67
 
46
68
  def use_ssl?
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scnnr
4
+ class Coordinate
5
+ attr_reader :items
6
+
7
+ def initialize(attrs = {})
8
+ @items = attrs['items'].map { |item| Coordinate::Item.new(item) }
9
+ end
10
+
11
+ def to_h
12
+ { 'items' => self.items.map(&:to_h) }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scnnr
4
+ class Coordinate
5
+ class Item
6
+ attr_reader :category, :labels
7
+
8
+ def initialize(attrs = {})
9
+ @category = attrs['category']
10
+ @labels = (attrs['labels'] || []).map { |label| Label.new('name' => label) }
11
+ end
12
+
13
+ def to_h
14
+ { 'category' => self.category, 'labels' => self.labels.map(&:to_h) }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -17,11 +17,12 @@ module Scnnr
17
17
  class RecognitionNotFound < Error; end
18
18
 
19
19
  class RecognitionFailed < Error
20
- attr_accessor :recognition
20
+ attr_accessor :recognition, :image
21
21
 
22
22
  def initialize(recognition)
23
23
  super(recognition.error)
24
24
  @recognition = recognition
25
+ @image = Scnnr::Errors::Image.new(recognition.error['image']) if recognition.error['image']
25
26
  end
26
27
  end
27
28
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scnnr
4
+ module Errors
5
+ class Image
6
+ attr_accessor :url, :response
7
+
8
+ def initialize(attrs)
9
+ @response = Response.new(attrs['response'])
10
+ @url = attrs['url']
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scnnr
4
+ module Errors
5
+ class Image
6
+ class Response
7
+ attr_accessor :status
8
+
9
+ def initialize(attrs)
10
+ @status = attrs['status']
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scnnr
4
+ class Label
5
+ attr_reader :name, :score
6
+
7
+ def initialize(attrs = {})
8
+ @name = attrs['name']
9
+ @score = attrs['score']
10
+ end
11
+
12
+ def to_h
13
+ { 'name' => self.name, 'score' => self.score }
14
+ end
15
+ end
16
+ end
@@ -2,10 +2,11 @@
2
2
 
3
3
  module Scnnr
4
4
  class Recognition
5
- attr_reader :id, :objects, :state, :error
5
+ attr_reader :id, :image, :objects, :state, :error
6
6
 
7
7
  def initialize(attrs = {})
8
8
  @id = attrs['id']
9
+ @image = Image.new(attrs['image']) if attrs['image']
9
10
  @objects = (attrs['objects'] || []).map { |obj| Object.new(obj) }
10
11
  @state = attrs['state']&.intern
11
12
  @error = attrs['error']
@@ -24,7 +25,12 @@ module Scnnr
24
25
  end
25
26
 
26
27
  def to_h
27
- { 'id' => self.id, 'objects' => self.objects.map(&:to_h), 'state' => self.state.to_s }
28
+ {
29
+ 'id' => self.id,
30
+ 'image' => self.image&.to_h,
31
+ 'objects' => self.objects.map(&:to_h),
32
+ 'state' => self.state.to_s,
33
+ }
28
34
  end
29
35
  end
30
36
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scnnr
4
+ class Recognition
5
+ class Image
6
+ attr_reader :url, :size
7
+
8
+ def initialize(attrs = {})
9
+ @url = attrs['url']
10
+ @size = attrs['size']
11
+ end
12
+
13
+ def to_h
14
+ { 'url' => self.url, 'size' => self.size }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -8,7 +8,7 @@ module Scnnr
8
8
  def initialize(attrs = {})
9
9
  @bounding_box = BoundingBox.new(attrs['bounding_box'])
10
10
  @category = attrs['category']
11
- @labels = (attrs['labels'] || []).map { |label| Label.new(label) }
11
+ @labels = (attrs['labels'] || []).map { |label| Scnnr::Label.new(label) }
12
12
  end
13
13
 
14
14
  def to_h
@@ -3,16 +3,10 @@
3
3
  module Scnnr
4
4
  class Recognition
5
5
  class Object
6
- class Label
7
- attr_reader :name, :score
8
-
9
- def initialize(attrs = {})
10
- @name = attrs['name']
11
- @score = attrs['score']
12
- end
13
-
14
- def to_h
15
- { 'name' => self.name, 'score' => self.score }
6
+ class Label < Scnnr::Label
7
+ def initialize(*args)
8
+ warn "[DEPRECATION] `#{self.class.name}` is deprecated. Please use `Scnnr::Label` instead."
9
+ super
16
10
  end
17
11
  end
18
12
  end
@@ -6,6 +6,7 @@ module Scnnr
6
6
 
7
7
  def initialize(response)
8
8
  raise UnexpectedError, response if response.content_type != SUPPORTED_CONTENT_TYPE
9
+
9
10
  @response = response
10
11
  end
11
12
 
@@ -18,13 +19,14 @@ module Scnnr
18
19
  end
19
20
 
20
21
  def build_recognition
21
- case @response
22
- when Net::HTTPSuccess
23
- recognition = Recognition.new(self.parsed_body)
24
- handle_recognition(recognition)
25
- else
26
- handle_error
27
- end
22
+ raise_error_response unless @response.is_a? Net::HTTPSuccess
23
+ recognition = Recognition.new(self.parsed_body)
24
+ handle_recognition(recognition)
25
+ end
26
+
27
+ def build_coordinate
28
+ raise_error_response unless @response.is_a? Net::HTTPSuccess
29
+ Coordinate.new(self.parsed_body)
28
30
  end
29
31
 
30
32
  private
@@ -35,11 +37,13 @@ module Scnnr
35
37
  case recognition.error['type']
36
38
  when 'unexpected-content', 'bad-request'
37
39
  raise RequestFailed, recognition.error
38
- else raise RecognitionFailed, recognition
40
+ when 'download-timeout', 'invalid-image', 'download-failed'
41
+ raise RecognitionFailed, recognition
42
+ else raise UnexpectedError, @response
39
43
  end
40
44
  end
41
45
 
42
- def handle_error
46
+ def raise_error_response
43
47
  case @response
44
48
  when Net::HTTPNotFound
45
49
  raise RecognitionNotFound, self.parsed_body
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scnnr
4
+ class Routing
5
+ API_SCHEME = URI::HTTPS
6
+ API_HOST = 'api.scnnr.cubki.jp'
7
+
8
+ attr_reader :path_prefix
9
+
10
+ def initialize(path, path_prefix, params, allowed_params)
11
+ @path = path
12
+ @path_prefix = path_prefix
13
+ @queries = build_queries params, allowed_params
14
+ end
15
+
16
+ def to_url
17
+ API_SCHEME.build(
18
+ host: API_HOST,
19
+ path: self.path,
20
+ query: query_string
21
+ )
22
+ end
23
+
24
+ def queries
25
+ @queries.reject { |_, val| val.nil? }
26
+ end
27
+
28
+ def path
29
+ "/#{[self.path_prefix, @path]
30
+ .map { |value| value.sub(%r{\A/}, '').sub(%r{/\z}, '') }
31
+ .join('/')}"
32
+ end
33
+
34
+ private
35
+
36
+ def query_string
37
+ params = self.queries
38
+ return if params.empty?
39
+
40
+ params
41
+ .map { |pair| pair.map { |val| URI.encode_www_form_component val }.join('=') }
42
+ .join('&')
43
+ end
44
+
45
+ def build_queries(params, allowed_params)
46
+ {}.tap do |queries|
47
+ (allowed_params || []).each do |param|
48
+ case param.intern
49
+ when :timeout then queries[:timeout] = params[:timeout] if params[:timeout]&.positive?
50
+ else queries[param] = params[param]
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Scnnr
4
- VERSION = '1.0.0'
4
+ VERSION = '2.0.0'
5
5
  end
@@ -1,7 +1,6 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
5
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
5
  require 'scnnr/version'
7
6
 
@@ -23,5 +22,7 @@ Gem::Specification.new do |spec|
23
22
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
24
23
  spec.require_paths = ['lib']
25
24
 
26
- spec.add_development_dependency 'bundler', '~> 1.15'
25
+ spec.add_development_dependency 'bundler', '~> 2.1'
26
+
27
+ spec.required_ruby_version = '>= 2.4'
27
28
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scnnr
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - NEWROPE Co. Ltd.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-11-02 00:00:00.000000000 Z
11
+ date: 2020-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.15'
19
+ version: '2.1'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.15'
26
+ version: '2.1'
27
27
  description: 'Official #CBK scnnr client library for Ruby.'
28
28
  email:
29
29
  - support@newrope.biz
@@ -39,6 +39,7 @@ files:
39
39
  - CHANGELOG.md
40
40
  - CODE_OF_CONDUCT.md
41
41
  - Gemfile
42
+ - Guardfile
42
43
  - LICENSE
43
44
  - README.md
44
45
  - Rakefile
@@ -48,13 +49,20 @@ files:
48
49
  - lib/scnnr/client.rb
49
50
  - lib/scnnr/configuration.rb
50
51
  - lib/scnnr/connection.rb
52
+ - lib/scnnr/coordinate.rb
53
+ - lib/scnnr/coordinate/item.rb
51
54
  - lib/scnnr/errors.rb
55
+ - lib/scnnr/errors/image.rb
56
+ - lib/scnnr/errors/image/response.rb
57
+ - lib/scnnr/label.rb
52
58
  - lib/scnnr/polling_manager.rb
53
59
  - lib/scnnr/recognition.rb
60
+ - lib/scnnr/recognition/image.rb
54
61
  - lib/scnnr/recognition/object.rb
55
62
  - lib/scnnr/recognition/object/bounding_box.rb
56
63
  - lib/scnnr/recognition/object/label.rb
57
64
  - lib/scnnr/response.rb
65
+ - lib/scnnr/routing.rb
58
66
  - lib/scnnr/version.rb
59
67
  - scnnr.gemspec
60
68
  homepage: https://github.com/NEWROPE/scnnr-ruby
@@ -69,15 +77,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
69
77
  requirements:
70
78
  - - ">="
71
79
  - !ruby/object:Gem::Version
72
- version: '0'
80
+ version: '2.4'
73
81
  required_rubygems_version: !ruby/object:Gem::Requirement
74
82
  requirements:
75
83
  - - ">="
76
84
  - !ruby/object:Gem::Version
77
85
  version: '0'
78
86
  requirements: []
79
- rubyforge_project:
80
- rubygems_version: 2.6.13
87
+ rubygems_version: 3.0.1
81
88
  signing_key:
82
89
  specification_version: 4
83
90
  summary: ''