diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..83e16f8 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..308b1c7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: ruby +rvm: + - 2.3.0 + - 2.5.0 +notifications: + email: false diff --git a/README.md b/README.md index 4f658eb..b50b644 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ # Scale -![Scale Logo](https://scale.ai/static/global/facebook-card.png) +![Scale Logo](https://scale.com/static/global/facebook-card.png) This is the official Scale RubyGem (`scaleapi`). -[Scale](https://scale.ai) is an API for Human Intelligence. Businesses like Alphabet (Google), Uber, Proctor & Gamble, Houzz, and many more use us to power tasks such as: +[Scale](https://scale.com) is an API for Human Intelligence. Businesses like Alphabet (Google), Uber, Proctor & Gamble, Houzz, and many more use us to power tasks such as: - Draw bounding boxes and label parts of images (to train ML algorithms for self-driving cars) - Transcribe documents, images, and webpages - Scrape websites - Triage support tickets - Categorize and compare images, documents, and webpages -Scale is actively hiring software engineers - [apply here](https://scale.ai/about#jobs). +Scale is actively hiring software engineers - [apply here](https://scale.com/about#jobs). ## Installation @@ -40,7 +40,7 @@ scale = Scale.new(api_key: 'SCALE_API_KEY') Note that you can optionally provide a `callback_auth_key` and `callback_url` when initializing the Scale client. You can also set `default_request_params` which is a `Hash` that will be included in every request sent to Scale (either as a query string param or part of the request body). -If you're having trouble finding your API Key or Callback Auth Key, then go to the [Scale Dashboard](https://scale.ai/dashboard). If you set a default `callback_url` in your account settings, you won't need to pass it in everytime. +If you're having trouble finding your API Key or Callback Auth Key, then go to the [Scale Dashboard](https://scale.com/dashboard). If you set a default `callback_url` in your account settings, you won't need to pass it in everytime. ## Creating Tasks @@ -51,9 +51,9 @@ For every type of task, you can pass in the following options when creating: - `urgency`: a string indicating how long the task should take, options are `immediate`, `day`, or `week`. The default is `day`. - `metadata`: a `Hash` that contains anything you want in it. Use it for storing data relevant to that task, such as an internal ID for your application to associate the task with. Note that the keys of the `Hash` will be returned as `String` rather than `Symbol`. -### Categoriation Tasks +### Categorization Tasks -To create a [categorization task](https://docs.scale.ai/#create-categorization-task), run the following: +To create a [categorization task](https://docs.scale.com/#create-categorization-task), run the following: ```ruby require 'scale' scale = Scale.new(api_key: 'SCALE_API_KEY') @@ -86,11 +86,11 @@ scale.tasks.create({ This will also return a `Scale::Api::Tasks::Categorization` object. -[Read more about creating categorization tasks](https://docs.scale.ai/#create-categorization-task) +[Read more about creating categorization tasks](https://docs.scale.com/#create-categorization-task) ### Comparison Tasks -To create a [comparison task](https://docs.scale.ai/#create-comparison-task), run the following: +To create a [comparison task](https://docs.scale.com/#create-comparison-task), run the following: ```ruby require 'scale' scale = Scale.new(api_key: 'SCALE_API_KEY') @@ -129,11 +129,11 @@ scale.tasks.create({ This will also return a `Scale::Api::Tasks::Comparison` object. -[Read more about creating comparison tasks](https://docs.scale.ai/#create-comparison-task) +[Read more about creating comparison tasks](https://docs.scale.com/#create-comparison-task) ### Datacollection Tasks -To create a [datacollection task](https://docs.scale.ai/#create-datacollection-task), run the following: +To create a [datacollection task](https://docs.scale.com/#create-datacollection-task), run the following: ```ruby require 'scale' scale = Scale.new(api_key: 'SCALE_API_KEY') @@ -141,7 +141,7 @@ scale = Scale.new(api_key: 'SCALE_API_KEY') scale.create_datacollection_task({ callback_url: 'http://www.example.com/callback', instruction: 'Find the URL for the hiring page for the company with attached website.', - attachment: 'https://scale.ai/', + attachment: 'https://scale.com/', attachment_type: 'website', fields: { hiring_page: 'Hiring Page URL' @@ -160,7 +160,7 @@ scale.tasks.create({ type: 'datacollection', callback_url: 'http://www.example.com/callback', instruction: 'Find the URL for the hiring page for the company with attached website.', - attachment: 'https://scale.ai/', + attachment: 'https://scale.com/', attachment_type: 'website', fields: { hiring_page: 'Hiring Page URL' @@ -170,12 +170,12 @@ scale.tasks.create({ This will also return a `Scale::Api::Tasks::Datacollection` object. -[Read more about creating datacollection tasks](https://docs.scale.ai/#create-data-collection-task) +[Read more about creating datacollection tasks](https://docs.scale.com/#create-data-collection-task) ### Image Recognition Tasks -To create an [image recognition task](https://docs.scale.ai/#create-image-recognition-task), run the following: +To create an [image recognition task](https://docs.scale.com/#create-image-recognition-task), run the following: ```ruby require 'scale' scale = Scale.new(api_key: 'SCALE_API_KEY') @@ -189,12 +189,12 @@ scale.create_annotation_task({ with_labels: true, examples: [ { - correct: false, + correct: true, image: 'http://i.imgur.com/lj6e98s.jpg', explanation: 'The boxes are tight and accurate' }, { - correct: true, + correct: false, image: 'http://i.imgur.com/HIrvIDq.jpg', explanation: 'The boxes are neither accurate nor complete' } @@ -221,12 +221,12 @@ scale.tasks.create({ with_labels: true, examples: [ { - correct: false, + correct: true, image: 'http://i.imgur.com/lj6e98s.jpg', explanation: 'The boxes are tight and accurate' }, { - correct: true, + correct: false, image: 'http://i.imgur.com/HIrvIDq.jpg', explanation: 'The boxes are neither accurate nor complete' } @@ -236,11 +236,11 @@ scale.tasks.create({ This will also return a `Scale::Api::Tasks::ImageRecognition` object. -[Read more about creating image recognition tasks](https://docs.scale.ai/#create-image-recognition-task) +[Read more about creating image recognition tasks](https://docs.scale.com/#create-image-recognition-task) ### Transcription Tasks -To create a [transcription task](https://docs.scale.ai/#create-transcription-task), run the following: +To create a [transcription task](https://docs.scale.com/#create-transcription-task), run the following: ```ruby require 'scale' scale = Scale.new(api_key: 'SCALE_API_KEY') @@ -278,12 +278,12 @@ scale.tasks.create({ This will also return a `Scale::Api::Tasks::Transcription` object. -[Read more about creating transcription tasks](https://docs.scale.ai/#create-transcription-task) +[Read more about creating transcription tasks](https://docs.scale.com/#create-transcription-task) ### Audio Transcription Tasks -To create an [audio transcription task](https://docs.scale.ai/#create-audio-transcription-task), run the following: +To create an [audio transcription task](https://docs.scale.com/#create-audio-transcription-task), run the following: ```ruby require 'scale' scale = Scale.new(api_key: 'SCALE_API_KEY') @@ -314,7 +314,7 @@ scale.tasks.create({ This will also return a `Scale::Api::Tasks::AudioTranscription` object. -[Read more about creating audio transcription tasks](https://docs.scale.ai/#create-audio-transcription-task) +[Read more about creating audio transcription tasks](https://docs.scale.com/#create-audio-transcription-task) ## Listing Tasks @@ -383,7 +383,7 @@ second_page = first_page.next_page `scale.tasks.list` is aliased to `scale.tasks.where` and `scale.tasks.all`. -For more information, [read our documentation](https://docs.scale.ai/#list-all-tasks) +For more information, [read our documentation](https://docs.scale.com/#list-all-tasks) ## Finding tasks by ID @@ -510,13 +510,17 @@ After checking out the repo, run `bin/setup` to install dependencies. You can al To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `scaleapi-ruby.gemspec`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). +To test locally, run `bundle exec rspec`. By default, client - server communication is mocked by [vcr](https://github.com/vcr/vcr), which uses local [casettes](spec/cassettes) recorded from production server. +To test against production server, set a test api key as `SCALE_TEST_API_KEY` and run `bundle exec rake spec:production`, which sets environment variable `SCALE_TEST_SERVER` to `production`. It will also update new client - server communication in the cassette. +Please note that changes in the cassettes will break existing specs. This is because 1) the server may return random results for test api key, and 2) some of the cassettes have been modified specifically for the specs. + ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/scaleapi/scaleapi-ruby. -Currently, this repository has no tests - and adding tests using RSpec would make a for a great PR :) -Thanks to [wikiti](https://github.com/wikiti/) for creating the first unofficial Scale Ruby Client! +Thanks to [wikiti](https://github.com/wikiti/) for creating the first unofficial Scale API Ruby Client! + ## License diff --git a/Rakefile b/Rakefile index 43022f7..ad5cea5 100644 --- a/Rakefile +++ b/Rakefile @@ -1,2 +1,7 @@ require "bundler/gem_tasks" -task :default => :spec +require 'rspec/core/rake_task' + +import './tasks/spec_production.rake' + +RSpec::Core::RakeTask.new(:spec) +task default: :spec diff --git a/lib/scale.rb b/lib/scale.rb index 4276011..5d33698 100644 --- a/lib/scale.rb +++ b/lib/scale.rb @@ -2,17 +2,22 @@ class Scale attr_accessor :api_key, :callback_auth_key, :default_request_params, :logging VALID_TASK_TYPES = [ - "datacollection", + "annotation", + "audiotranscription", "categorization", "comparison", - "annotation", - "polygonannotation", + "cuboidannotation", + "datacollection", + "imageannotation", "lineannotation", - "transcription", - "audiotranscription", + "namedentityrecognition", "pointannotation", - "cuboidannotation", - "segmentannotation" + "polygonannotation", + "segmentannotation", + "transcription", + "videoannotation", + "videoboxannotation", + "videocuboidannotation" ].freeze def method_missing(methodId, *args, &block) diff --git a/lib/scale/api.rb b/lib/scale/api.rb index f25748d..06ea484 100644 --- a/lib/scale/api.rb +++ b/lib/scale/api.rb @@ -4,7 +4,7 @@ class Scale class Api < Struct.new(:api_key, :callback_auth_key, :default_request_params, :logging) - SCALE_API_URL = 'https://api.scale.ai/v1/' + SCALE_API_URL = 'https://api.scale.com/v1/' SCALEAPI_GEM_INFO = Gem.loaded_specs["scaleapi"] SCALE_RUBY_CLIENT_VERSION = SCALEAPI_GEM_INFO ? SCALEAPI_GEM_INFO.version.to_s : '0.1.1'.freeze diff --git a/lib/scale/api/task_list.rb b/lib/scale/api/task_list.rb index 14ee5f3..f2e85f9 100644 --- a/lib/scale/api/task_list.rb +++ b/lib/scale/api/task_list.rb @@ -25,11 +25,10 @@ def has_more? end def page - (offset + (limit * 1)) / limit + (offset + limit) / limit end def next_page - next_page_params = params.dup params[:offset] = params[:limit] + params[:offset] Scale::Api::Tasks.new(client).list(params) end diff --git a/scaleapi-ruby.gemspec b/scaleapi-ruby.gemspec index 8b766d0..a7d3f49 100644 --- a/scaleapi-ruby.gemspec +++ b/scaleapi-ruby.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |spec| spec.summary = %q{Official Ruby Client for Scale API} spec.description = %q{Scale is an API For Human Intelligence. Get high quality results for all sorts of tasks within minutes. This is the official Ruby client.} - spec.homepage = "https://scale.ai" + spec.homepage = "https://scale.com" spec.license = "MIT" @@ -23,5 +23,9 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler", "~> 1.11" spec.add_development_dependency "rake", "~> 10.0" + spec.add_development_dependency "rspec", "~> 3.8" + spec.add_development_dependency "webmock", "~> 3.5" + spec.add_development_dependency "vcr", "~> 4.0" + spec.add_dependency "faraday", "~> 0.11.0" end diff --git a/spec/cassettes/audio_transcription.yml b/spec/cassettes/audio_transcription.yml new file mode 100644 index 0000000..abafdd2 --- /dev/null +++ b/spec/cassettes/audio_transcription.yml @@ -0,0 +1,20 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.scale.com/v1/task/audiotranscription + body: + encoding: UTF-8 + string: '{"callback_url":"http://www.example.com/callback","attachment_type":"audio","attachment":"https://storage.googleapis.com/deepmind-media/pixie/knowing-what-to-say/second-list/speaker-3.wav","verbatim":false}' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"task_id":"5c84d0efc4fd4f0075d11f8c","created_at":"2019-03-10T08:55:12.058Z","completed_at":"2019-03-10T08:55:12.111Z","callback_url":"http://www.example.com/callback","type":"audiotranscription","status":"completed","instruction":"Please + transcribe the attached audio file.","params":{"attachment":"https://storage.googleapis.com/deepmind-media/pixie/knowing-what-to-say/second-list/speaker-3.wav","attachment_type":"audio","verbatim":false,"skip_human_transcription":false},"is_test":true,"urgency":"standard","metadata":{},"callback_succeeded":true,"processed_attachments":[],"response":{"transcript":"This + is an example transcript using your Scale test key.","duration":5,"alignment":[{"word":"This","start":0.3,"end":0.37,"confidence":0.9},{"word":"is","start":0.37,"end":0.79,"confidence":0.9},{"word":"an","start":0.79,"end":0.94,"confidence":0.9},{"word":"example","start":0.94,"end":1,"confidence":0.9},{"word":"transcript","start":1,"end":1.3,"confidence":0.3},{"word":"using","start":1.3,"end":1.59,"confidence":0.3},{"word":"your","start":1.59,"end":1.8,"confidence":0.9},{"word":"Scale","start":1.81,"end":2.01,"confidence":0.9},{"word":"API","start":2.01,"end":2.37,"confidence":0.9},{"word":"test","start":2.37,"end":2.82,"confidence":0.9},{"word":"key","start":2.85,"end":3.11,"confidence":0.9}]}}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:55:12 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/categorization.yml b/spec/cassettes/categorization.yml new file mode 100644 index 0000000..f5d559a --- /dev/null +++ b/spec/cassettes/categorization.yml @@ -0,0 +1,20 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.scale.com/v1/task/categorization + body: + encoding: UTF-8 + string: '{"callback_url":"http://www.example.com/callback","instruction":"Is + this company public or private?","attachment_type":"website","attachment":"https://www.google.com","categories":["public","private"]}' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"task_id":"5c84d0fe958593004f13e5ba","created_at":"2019-03-10T08:55:26.624Z","completed_at":"2019-03-10T08:55:26.669Z","callback_url":"http://www.example.com/callback","type":"categorization","status":"completed","instruction":"Is + this company public or private?","params":{"attachment_type":"website","attachment":"https://www.google.com","categories":["public","private"],"allow_multiple":false},"is_test":true,"urgency":"standard","metadata":{},"callback_succeeded":true,"processed_attachments":[],"response":{"category":"public"}}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:55:26 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/comparison.yml b/spec/cassettes/comparison.yml new file mode 100644 index 0000000..10644fc --- /dev/null +++ b/spec/cassettes/comparison.yml @@ -0,0 +1,20 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.scale.com/v1/task/comparison + body: + encoding: UTF-8 + string: '{"callback_url":"http://www.example.com/callback","instruction":"Do + the objects in these images have the same pattern?","attachments":["http://i.ebayimg.com/00/$T2eC16dHJGwFFZKjy5ZjBRfNyMC4Ig~~_32.JPG","http://images.wisegeek.com/checkered-tablecloth.jpg"],"attachment_type":"image","choices":["yes","no"]}' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"task_id":"5c84d0f367764e00561a72eb","created_at":"2019-03-10T08:55:15.129Z","completed_at":"2019-03-10T08:55:15.174Z","callback_url":"http://www.example.com/callback","type":"comparison","status":"completed","instruction":"Do + the objects in these images have the same pattern?","params":{"attachments":["http://i.ebayimg.com/00/$T2eC16dHJGwFFZKjy5ZjBRfNyMC4Ig~~_32.JPG","http://images.wisegeek.com/checkered-tablecloth.jpg"],"attachment_type":"image","choices":["yes","no"]},"is_test":true,"urgency":"standard","metadata":{},"callback_succeeded":true,"processed_attachments":[],"response":{"choice":"yes"}}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:55:15 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/cuboid_annotation.yml b/spec/cassettes/cuboid_annotation.yml new file mode 100644 index 0000000..6cf0f31 --- /dev/null +++ b/spec/cassettes/cuboid_annotation.yml @@ -0,0 +1,20 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.scale.com/v1/task/cuboidannotation + body: + encoding: UTF-8 + string: '{"callback_url":"http://www.example.com/callback","instruction":"Draw + a cuboid around each car or truck.","attachment_type":"image","attachment":"http://i.imgur.com/v4cBreD.jpg","objects_to_annotate":["car","truck"]}' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"task_id":"5c84d0f32e5be1004a45c510","created_at":"2019-03-10T08:55:16.082Z","completed_at":"2019-03-10T08:55:16.128Z","callback_url":"http://www.example.com/callback","type":"cuboidannotation","status":"completed","instruction":"Draw + a cuboid around each car or truck.","params":{"attachment":"http://i.imgur.com/v4cBreD.jpg","attachment_type":"image","objects_to_annotate":["car","truck"],"with_labels":true,"min_annotations":0,"max_annotations":0},"is_test":true,"urgency":"standard","metadata":{},"callback_succeeded":true,"processed_attachments":[],"response":{"annotations":[{"vertices":[{"description":"face-topleft","y":70,"x":100,"type":"vertex"},{"description":"face-bottomleft","y":296,"x":100,"type":"vertex"},{"description":"face-topright","y":70,"x":320,"type":"vertex"},{"description":"face-bottomright","y":296,"x":320,"type":"vertex"},{"description":"side-topcorner","y":82,"x":441,"type":"vertex"},{"description":"side-bottomcorner","y":198,"x":441,"type":"vertex"}],"label":"car"}]}}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:55:16 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/custom_serializer.rb b/spec/cassettes/custom_serializer.rb new file mode 100644 index 0000000..d69d6e0 --- /dev/null +++ b/spec/cassettes/custom_serializer.rb @@ -0,0 +1,17 @@ +require 'yaml' +require 'vcr/cassette/serializers/yaml' + +# Remove request.headers and response.headers from the cassette, +# which contain sensitive information (e.g. authentication hash). +module Cassettes + class CustomSerializer + extend VCR::Cassette::EncodingErrorHandling + extend VCR::Cassette::Serializers::YAML + + def self.serialize(hash) + hash['http_interactions'][0]['request'].delete('headers') + hash['http_interactions'][0]['response'].delete('headers') + VCR::Cassette::Serializers::YAML.serialize(hash) + end + end +end diff --git a/spec/cassettes/data_collection.yml b/spec/cassettes/data_collection.yml new file mode 100644 index 0000000..7173daf --- /dev/null +++ b/spec/cassettes/data_collection.yml @@ -0,0 +1,23 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.scale.com/v1/task/datacollection + body: + encoding: UTF-8 + string: '{"callback_url":"http://www.example.com/callback","instruction":"Find + the URL for the hiring page for the company with attached website.","attachment":"https://scale.com/","attachment_type":"website","fields":{"hiring_page":"Hiring + Page URL"}}' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"task_id":"5c84d0f562e7490079ffae3b","created_at":"2019-03-10T08:55:17.126Z","completed_at":"2019-03-10T08:55:17.173Z","callback_url":"http://www.example.com/callback","type":"datacollection","status":"completed","instruction":"Find + the URL for the hiring page for the company with attached website.","params":{"attachment_type":"website","attachment":"https://scale.com/","fields":{"hiring_page":"Hiring + Page URL"}},"is_test":true,"urgency":"standard","metadata":{},"callback_succeeded":true,"processed_attachments":[],"response":{"fields":{"hiring_page":"test + response"},"time_spent_in_seconds":15}}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:55:17 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/image_recognition.yml b/spec/cassettes/image_recognition.yml new file mode 100644 index 0000000..5258dd9 --- /dev/null +++ b/spec/cassettes/image_recognition.yml @@ -0,0 +1,27 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.scale.com/v1/task/annotation + body: + encoding: UTF-8 + string: '{"callback_url":"http://www.example.com/callback","instruction":"Draw + a box around each **baby cow** and **big cow**","attachment_type":"image","attachment":"http://i.imgur.com/v4cBreD.jpg","objects_to_annotate":["baby + cow","big cow"],"with_labels":true,"examples":[{"correct":true,"image":"http://i.imgur.com/lj6e98s.jpg","explanation":"The + boxes are tight and accurate"},{"correct":false,"image":"http://i.imgur.com/HIrvIDq.jpg","explanation":"The + boxes are neither accurate nor complete"}]}' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"task_id":"5c84d0f50413320064109f34","created_at":"2019-03-10T08:55:18.054Z","completed_at":"2019-03-10T08:55:18.100Z","callback_url":"http://www.example.com/callback","type":"annotation","status":"completed","instruction":"Draw + a box around each **baby cow** and **big cow**","params":{"attachment":"http://i.imgur.com/v4cBreD.jpg","attachment_type":"image","objects_to_annotate":["baby + cow","big cow"],"with_labels":true,"min_width":0,"min_height":0,"examples":[{"correct":true,"image":"http://i.imgur.com/lj6e98s.jpg","explanation":"The + boxes are tight and accurate"},{"correct":false,"image":"http://i.imgur.com/HIrvIDq.jpg","explanation":"The + boxes are neither accurate nor complete"}]},"is_test":true,"urgency":"standard","metadata":{},"callback_succeeded":true,"processed_attachments":[],"response":{"annotations":[{"left":0,"top":0,"width":100,"height":100,"label":"big + cow"}]}}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:55:18 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/line_annotation.yml b/spec/cassettes/line_annotation.yml new file mode 100644 index 0000000..2f4ead5 --- /dev/null +++ b/spec/cassettes/line_annotation.yml @@ -0,0 +1,23 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.scale.com/v1/task/lineannotation + body: + encoding: UTF-8 + string: '{"callback_url":"http://www.example.com/callback","instruction":"Draw + a tight shape around the big cow","attachment_type":"image","attachment":"http://i.imgur.com/v4cBreD.jpg","objects_to_annotate":["big + cow"],"with_labels":true}' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"task_id":"5c84d0f61fd428005d20e8dc","created_at":"2019-03-10T08:55:18.928Z","completed_at":"2019-03-10T08:55:18.978Z","callback_url":"http://www.example.com/callback","type":"lineannotation","status":"completed","instruction":"Draw + a tight shape around the big cow","params":{"attachment":"http://i.imgur.com/v4cBreD.jpg","attachment_type":"image","objects_to_annotate":["big + cow"],"with_labels":true,"splines":false},"is_test":true,"urgency":"standard","metadata":{},"callback_succeeded":true,"processed_attachments":[],"response":{"annotations":[{"vertices":[{"x":10,"y":10},{"x":110,"y":110},{"x":10,"y":110}],"label":"big + cow"}]}}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:55:19 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/point_annotation.yml b/spec/cassettes/point_annotation.yml new file mode 100644 index 0000000..c513a6b --- /dev/null +++ b/spec/cassettes/point_annotation.yml @@ -0,0 +1,20 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.scale.com/v1/task/pointannotation + body: + encoding: UTF-8 + string: '{"callback_url":"http://www.example.com/callback","instruction":"Draw + a point on every **headlight** and **brakelight** of a car in the image.","attachment_type":"image","attachment":"http://i.imgur.com/XOJbalC.jpg","objects_to_annotate":["headlight","brakelight"],"with_labels":true}' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"task_id":"5c84d0f74d286e0056a9c39a","created_at":"2019-03-10T08:55:19.964Z","completed_at":"2019-03-10T08:55:20.009Z","callback_url":"http://www.example.com/callback","type":"pointannotation","status":"completed","instruction":"Draw + a point on every **headlight** and **brakelight** of a car in the image.","params":{"attachment":"http://i.imgur.com/XOJbalC.jpg","attachment_type":"image","objects_to_annotate":["headlight","brakelight"],"with_labels":true},"is_test":true,"urgency":"standard","metadata":{},"callback_succeeded":true,"processed_attachments":[]}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:55:20 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/polygon_annotation.yml b/spec/cassettes/polygon_annotation.yml new file mode 100644 index 0000000..baa3b74 --- /dev/null +++ b/spec/cassettes/polygon_annotation.yml @@ -0,0 +1,23 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.scale.com/v1/task/polygonannotation + body: + encoding: UTF-8 + string: '{"callback_url":"http://www.example.com/callback","instruction":"Draw + a tight shape around the big cow","attachment_type":"image","attachment":"http://i.imgur.com/v4cBreD.jpg","objects_to_annotate":["big + cow"],"with_labels":true}' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"task_id":"5c84d0f862e7490079ffae3e","created_at":"2019-03-10T08:55:20.968Z","completed_at":"2019-03-10T08:55:21.015Z","callback_url":"http://www.example.com/callback","type":"polygonannotation","status":"completed","instruction":"Draw + a tight shape around the big cow","params":{"attachment":"http://i.imgur.com/v4cBreD.jpg","attachment_type":"image","objects_to_annotate":["big + cow"],"with_labels":true},"is_test":true,"urgency":"standard","metadata":{},"callback_succeeded":true,"processed_attachments":[],"response":{"annotations":[{"vertices":[{"x":10,"y":10},{"x":110,"y":110},{"x":10,"y":110}],"label":"big + cow"}]}}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:55:21 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/segment_annotation.yml b/spec/cassettes/segment_annotation.yml new file mode 100644 index 0000000..8b64904 --- /dev/null +++ b/spec/cassettes/segment_annotation.yml @@ -0,0 +1,23 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.scale.com/v1/task/segmentannotation + body: + encoding: UTF-8 + string: '{"callback_url":"http://www.example.com/callback","instruction":"Please + segment the image using the given labels.","attachment_type":"image","attachment":"http://i.imgur.com/XOJbalC.jpg","labels":["background","road","vegetation","lane + marking"],"instance_labels":["vehicle","pedestrian"],"allow_unlabeled":false}' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"task_id":"5c84d0f9e95619006b073a90","created_at":"2019-03-10T08:55:21.714Z","completed_at":"2019-03-10T08:55:23.259Z","callback_url":"http://www.example.com/callback","type":"segmentannotation","status":"completed","instruction":"Please + segment the image using the given labels.","params":{"attachment":"http://i.imgur.com/XOJbalC.jpg","attachment_type":"image","labels":["background","road","vegetation","lane + marking"],"instance_labels":["vehicle","pedestrian"],"allow_unlabeled":false,"allow_parent_labels":false},"is_test":true,"urgency":"standard","metadata":{},"callback_succeeded":true,"processed_attachments":[],"response":{"annotations":{"unlabeled":"https://scaleapi-results.s3.us-west-1.amazonaws.com/7d7cfc4e-68e2-4a11-9e68-79795bd2d0eb","labeled":{"background":"https://scaleapi-results.s3.us-west-1.amazonaws.com/6587eb57-65a5-4a9b-9b89-67d16fafc425"},"combined":{"image":"https://scaleapi-results.s3.us-west-1.amazonaws.com/48dbfee9-e849-4aa6-b060-803c433676fc","indexedImage":"https://scaleapi-results.s3.us-west-1.amazonaws.com/dc9cd6c3-a4d2-4030-9fca-618aceb996ab"}},"labelMapping":{"background":{"index":1,"color":"#ff0000"},"road":{"index":2,"color":"#ffff00"},"vegetation":{"index":3,"color":"#00ff00"},"lane + marking":{"index":4,"color":"#00ffff"}}}}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:55:23 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/task_cancel_invalid.yml b/spec/cassettes/task_cancel_invalid.yml new file mode 100644 index 0000000..ad4db58 --- /dev/null +++ b/spec/cassettes/task_cancel_invalid.yml @@ -0,0 +1,18 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.scale.com/v1/task/invalid_task_id/cancel + body: + encoding: UTF-8 + string: '{"callback_url":null}' + response: + status: + code: 404 + message: Not Found + body: + encoding: ASCII-8BIT + string: '{"status_code":404,"error":"Task with that ID not found."}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:55:27 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/task_cancel_valid.yml b/spec/cassettes/task_cancel_valid.yml new file mode 100644 index 0000000..9214f9e --- /dev/null +++ b/spec/cassettes/task_cancel_valid.yml @@ -0,0 +1,19 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.scale.com/v1/task/5c84d0fe958593004f13e5ba/cancel + body: + encoding: US-ASCII + string: '{"callback_url":null}' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"task_id":"5c84d0fe958593004f13e5ba","created_at":"2019-03-10T08:55:26.624Z","completed_at":"2019-03-10T08:55:26.669Z","callback_url":"http://www.example.com/callback","type":"categorization","status":"canceled","instruction":"Is + this company public or private?","params":{"attachment_type":"website","attachment":"https://www.google.com","categories":["public","private"],"allow_multiple":false},"is_test":true,"urgency":"standard","metadata":{},"callback_succeeded":true,"processed_attachments":[],"response":{"category":"private","category_id":null}}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:58:56 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/task_find_invalid.yml b/spec/cassettes/task_find_invalid.yml new file mode 100644 index 0000000..5090513 --- /dev/null +++ b/spec/cassettes/task_find_invalid.yml @@ -0,0 +1,18 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.scale.com/v1/task/invalid_task_id?callback_url + body: + encoding: US-ASCII + string: '' + response: + status: + code: 404 + message: Not Found + body: + encoding: ASCII-8BIT + string: '{"status_code":404,"error":"Task with that ID not found."}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:55:26 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/task_find_valid.yml b/spec/cassettes/task_find_valid.yml new file mode 100644 index 0000000..3f95ed6 --- /dev/null +++ b/spec/cassettes/task_find_valid.yml @@ -0,0 +1,19 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.scale.com/v1/task/5c84d0fe958593004f13e5ba?callback_url + body: + encoding: US-ASCII + string: '' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"task_id":"5c84d0fe958593004f13e5ba","created_at":"2019-03-10T08:55:26.624Z","completed_at":"2019-03-10T08:55:26.669Z","callback_url":"http://www.example.com/callback","type":"categorization","status":"completed","instruction":"Is + this company public or private?","params":{"attachment_type":"website","attachment":"https://www.google.com","categories":["public","private"],"allow_multiple":false},"is_test":true,"urgency":"standard","metadata":{},"callback_succeeded":true,"processed_attachments":[],"response":{"category":"private","category_id":null}}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:58:56 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/task_list_1.yml b/spec/cassettes/task_list_1.yml new file mode 100644 index 0000000..bd6803c --- /dev/null +++ b/spec/cassettes/task_list_1.yml @@ -0,0 +1,23 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.scale.com/v1/tasks?callback_url&end_time&limit=2&offset=0&start_time&status&type + body: + encoding: US-ASCII + string: '' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"docs":[{"task_id":"5c849c3fe95619006b071dd4","created_at":"2019-03-10T05:10:23.260Z","completed_at":"2019-03-10T05:10:25.224Z","callback_url":"http://www.example.com/callback","type":"segmentannotation","status":"completed","instruction":"Please + segment the image using the given labels.","params":{"attachment":"http://i.imgur.com/XOJbalC.jpg","attachment_type":"image","labels":["background","road","vegetation","lane + marking"],"instance_labels":["vehicle","pedestrian"],"allow_unlabeled":false,"allow_parent_labels":false},"is_test":true,"urgency":"day","metadata":{},"processed_attachments":[],"response":{"annotations":{"unlabeled":"https://scaleapi-results.s3.amazonaws.com/0704357e-de4f-4247-b4fe-3347039624bb","labeled":{"background":"https://scaleapi-results.s3.amazonaws.com/e405b86f-3f47-41ab-9ea3-aaedd6f34c53","road":null,"vegetation":null,"lane + marking":null},"combined":{"image":"https://scaleapi-results.s3.amazonaws.com/dae72038-eb78-4e89-b994-1ca249ad888a","indexedImage":"https://scaleapi-results.s3.amazonaws.com/8d57af40-a7d5-4514-aff0-f033ab90b55f"}},"labelMapping":{"background":{"index":1,"color":"#ff0000","attributes":null},"road":{"index":2,"color":"#ffff00","attributes":null},"vegetation":{"index":3,"color":"#00ff00","attributes":null},"lane + marking":{"index":4,"color":"#00ffff","attributes":null}}}},{"task_id":"5c849b4fe95619006b071d5d","created_at":"2019-03-10T05:06:23.993Z","completed_at":"2019-03-10T05:06:24.039Z","callback_url":"http://www.example.com/callback","type":"cuboidannotation","status":"completed","instruction":"Draw + a cuboid around each car or truck.","params":{"attachment":"http://i.imgur.com/v4cBreD.jpg","attachment_type":"image","objects_to_annotate":["car","truck"],"with_labels":true,"min_annotations":0,"max_annotations":0},"is_test":true,"urgency":"day","metadata":{},"processed_attachments":[],"response":{"annotations":[{"vertices":[{"description":"face-topleft","y":70,"x":100,"type":"vertex"},{"description":"face-bottomleft","y":296,"x":100,"type":"vertex"},{"description":"face-topright","y":70,"x":320,"type":"vertex"},{"description":"face-bottomright","y":296,"x":320,"type":"vertex"},{"description":"side-topcorner","y":82,"x":441,"type":"vertex"},{"description":"side-bottomcorner","y":198,"x":441,"type":"vertex"}],"label":"car"}]}}],"total":3,"limit":2,"offset":0,"has_more":true}' + http_version: + recorded_at: Sun, 10 Mar 2019 06:00:56 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/task_list_2.yml b/spec/cassettes/task_list_2.yml new file mode 100644 index 0000000..3f05017 --- /dev/null +++ b/spec/cassettes/task_list_2.yml @@ -0,0 +1,19 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.scale.com/v1/tasks?callback_url&end_time&limit=2&offset=2&start_time&status&type + body: + encoding: US-ASCII + string: '' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"docs":[{"task_id":"5c8498e23beb09005d953d51","created_at":"2019-03-10T04:56:02.086Z","completed_at":"2019-03-10T04:56:02.133Z","callback_url":"http://www.example.com/callback","type":"pointannotation","status":"completed","instruction":"Draw + a point on every **headlight** and **brakelight** of a car in the image.","params":{"attachment":"http://i.imgur.com/XOJbalC.jpg","attachment_type":"image","objects_to_annotate":["headlight","brakelight"],"with_labels":true},"is_test":true,"urgency":"day","metadata":{},"processed_attachments":[]}],"total":3,"limit":2,"offset":2,"has_more":false}' + http_version: + recorded_at: Sun, 10 Mar 2019 06:00:57 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/cassettes/transcription.yml b/spec/cassettes/transcription.yml new file mode 100644 index 0000000..16e22e1 --- /dev/null +++ b/spec/cassettes/transcription.yml @@ -0,0 +1,23 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.scale.com/v1/task/transcription + body: + encoding: UTF-8 + string: '{"callback_url":"http://www.example.com/callback","instruction":"Transcribe + the given fields.","attachment_type":"website","attachment":"http://news.ycombinator.com/","fields":{"title":"Title + of Webpage","top_result":"Title of the top result"}}' + response: + status: + code: 200 + message: OK + body: + encoding: ASCII-8BIT + string: '{"task_id":"5c84d0fb26eb2700706a9069","created_at":"2019-03-10T08:55:24.068Z","completed_at":"2019-03-10T08:55:24.114Z","callback_url":"http://www.example.com/callback","type":"transcription","status":"completed","instruction":"Transcribe + the given fields.","params":{"attachment_type":"website","attachment":"http://news.ycombinator.com/","fields":{"title":"Title + of Webpage","top_result":"Title of the top result"}},"is_test":true,"urgency":"standard","metadata":{},"callback_succeeded":true,"processed_attachments":[],"response":{"fields":{"title":"test + response","top_result":"test response"},"repeatable_fields":[null,null]}}' + http_version: + recorded_at: Sun, 10 Mar 2019 08:55:24 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/lib/api/task_list_spec.rb b/spec/lib/api/task_list_spec.rb new file mode 100644 index 0000000..ded6bf8 --- /dev/null +++ b/spec/lib/api/task_list_spec.rb @@ -0,0 +1,45 @@ +RSpec.describe Scale::Api::TaskList do + let(:scale) { Scale.new(api_key: SCALE_TEST_API_KEY) } + + # the cassette assumes there are totally 3 tasks + # page 1 retrieves 2 tasks + let(:page_1) { VCR.use_cassette('task_list_1') { scale.tasks.list(limit: 2) } } + # page 2 retrieves the remaining 1 task. + let(:page_2) { VCR.use_cassette('task_list_2') { page_1.next_page } } + + it 'returns a task list' do + expect(page_1).to be_an_instance_of Scale::Api::TaskList + expect(page_2).to be_an_instance_of Scale::Api::TaskList + + expect(page_1.length).to eq 2 + expect(page_2.length).to eq 1 + end + + it 'is an enumerable of task' do + types = page_1.map(&:class).uniq + expect(types.size).to eq 1 + expect(types.first).to eq Scale::Api::Tasks::BaseTask + + types = page_2.map(&:class).uniq + expect(types.size).to eq 1 + expect(types.first).to eq Scale::Api::Tasks::BaseTask + end + + describe '#has_more?' do + it { expect(page_1.has_more?).to be true } + it { expect(page_2.has_more?).to be false } + end + + describe '#page' do + it { expect(page_1.page).to eq 1 } + it { expect(page_2.page).to eq 2 } + end + + describe '#next_page' do + it 'does not change limit' do + # page 2 inherits 2 as its limit from page 1 + expect(page_1.limit).to eq 2 + expect(page_2.limit).to eq 2 + end + end +end diff --git a/spec/lib/api/tasks/audio_transcription_task_spec.rb b/spec/lib/api/tasks/audio_transcription_task_spec.rb new file mode 100644 index 0000000..273d0c2 --- /dev/null +++ b/spec/lib/api/tasks/audio_transcription_task_spec.rb @@ -0,0 +1,16 @@ +RSpec.describe Scale::Api::Tasks, 'audio transcription' do + let(:scale) { Scale.new(api_key: SCALE_TEST_API_KEY) } + + it 'returns transcription for audio transcription task' do + response = VCR.use_cassette('audio_transcription') do + scale.create_audiotranscription_task( + callback_url: 'http://www.example.com/callback', + attachment_type: 'audio', + attachment: 'https://storage.googleapis.com/deepmind-media/pixie/knowing-what-to-say/second-list/speaker-3.wav', + verbatim: false) + end + + expect(response.type).to eq 'audiotranscription' + expect(response.response.keys).to contain_exactly('transcript', 'duration', 'alignment') + end +end diff --git a/spec/lib/api/tasks/categorization_task_spec.rb b/spec/lib/api/tasks/categorization_task_spec.rb new file mode 100644 index 0000000..80da6da --- /dev/null +++ b/spec/lib/api/tasks/categorization_task_spec.rb @@ -0,0 +1,17 @@ +RSpec.describe Scale::Api::Tasks, 'categorization' do + let(:scale) { Scale.new(api_key: SCALE_TEST_API_KEY) } + + it 'returns category for categorization task' do + response = VCR.use_cassette('categorization') do + scale.create_categorization_task( + callback_url: 'http://www.example.com/callback', + instruction: 'Is this company public or private?', + attachment_type: 'website', + attachment: 'https://www.google.com', + categories: %w(public private)) + end + + expect(response.type).to eq 'categorization' + expect(response.response['category']).to eq 'public' + end +end diff --git a/spec/lib/api/tasks/comparison_task_spec.rb b/spec/lib/api/tasks/comparison_task_spec.rb new file mode 100644 index 0000000..5b165a8 --- /dev/null +++ b/spec/lib/api/tasks/comparison_task_spec.rb @@ -0,0 +1,20 @@ +RSpec.describe Scale::Api::Tasks, 'comparison' do + let(:scale) { Scale.new(api_key: SCALE_TEST_API_KEY) } + + it 'returns choice for comparison task' do + response = VCR.use_cassette('comparison') do + scale.create_comparison_task( + callback_url: 'http://www.example.com/callback', + instruction: 'Do the objects in these images have the same pattern?', + attachments: [ + 'http://i.ebayimg.com/00/$T2eC16dHJGwFFZKjy5ZjBRfNyMC4Ig~~_32.JPG', + 'http://images.wisegeek.com/checkered-tablecloth.jpg' + ], + attachment_type: 'image', + choices: %w(yes no)) + end + + expect(response.type).to eq 'comparison' + expect(response.response['choice']).to eq 'yes' + end +end diff --git a/spec/lib/api/tasks/cuboid_annotation_task_spec.rb b/spec/lib/api/tasks/cuboid_annotation_task_spec.rb new file mode 100644 index 0000000..3117368 --- /dev/null +++ b/spec/lib/api/tasks/cuboid_annotation_task_spec.rb @@ -0,0 +1,19 @@ +RSpec.describe Scale::Api::Tasks, 'cuboid annotation' do + let(:scale) { Scale.new(api_key: SCALE_TEST_API_KEY) } + + it 'returns annotations for line cuboid annotation task' do + response = VCR.use_cassette('cuboid_annotation') do + scale.create_cuboidannotation_task({ + callback_url: 'http://www.example.com/callback', + instruction: 'Draw a cuboid around each car or truck.', + attachment_type: 'image', + attachment: 'http://i.imgur.com/v4cBreD.jpg', + objects_to_annotate: ['car', 'truck']}) + end + + expect(response.type).to eq 'cuboidannotation' + response.response['annotations'].each do |annotation| + expect(annotation.keys).to contain_exactly('vertices', "label") + end + end +end diff --git a/spec/lib/api/tasks/data_collection_task_spec.rb b/spec/lib/api/tasks/data_collection_task_spec.rb new file mode 100644 index 0000000..4d94886 --- /dev/null +++ b/spec/lib/api/tasks/data_collection_task_spec.rb @@ -0,0 +1,19 @@ +RSpec.describe Scale::Api::Tasks, 'data collection' do + let(:scale) { Scale.new(api_key: SCALE_TEST_API_KEY) } + + it 'returns fields for data collection task' do + response = VCR.use_cassette('data_collection') do + scale.create_datacollection_task( + callback_url: 'http://www.example.com/callback', + instruction: 'Find the URL for the hiring page for the company with attached website.', + attachment: 'https://scale.com/', + attachment_type: 'website', + fields: { + hiring_page: 'Hiring Page URL' + }) + end + + expect(response.type).to eq 'datacollection' + expect(response.response['fields']['hiring_page']).to eq 'test response' + end +end diff --git a/spec/lib/api/tasks/image_recognition_task_spec.rb b/spec/lib/api/tasks/image_recognition_task_spec.rb new file mode 100644 index 0000000..5967072 --- /dev/null +++ b/spec/lib/api/tasks/image_recognition_task_spec.rb @@ -0,0 +1,32 @@ +RSpec.describe Scale::Api::Tasks, 'image recognition' do + let(:scale) { Scale.new(api_key: SCALE_TEST_API_KEY) } + + it 'returns annotations for image recognition task' do + response = VCR.use_cassette('image_recognition') do + scale.create_annotation_task( + callback_url: 'http://www.example.com/callback', + instruction: 'Draw a box around each **baby cow** and **big cow**', + attachment_type: 'image', + attachment: 'http://i.imgur.com/v4cBreD.jpg', + objects_to_annotate: ['baby cow', 'big cow'], + with_labels: true, + examples: [ + { + correct: true, + image: 'http://i.imgur.com/lj6e98s.jpg', + explanation: 'The boxes are tight and accurate' + }, + { + correct: false, + image: 'http://i.imgur.com/HIrvIDq.jpg', + explanation: 'The boxes are neither accurate nor complete' + } + ]) + end + + expect(response.type).to eq 'annotation' + response.response['annotations'].each do |annotation| + expect(annotation.keys).to contain_exactly('left', 'top', 'width', 'height', 'label') + end + end +end diff --git a/spec/lib/api/tasks/line_annotation_task_spec.rb b/spec/lib/api/tasks/line_annotation_task_spec.rb new file mode 100644 index 0000000..8d07494 --- /dev/null +++ b/spec/lib/api/tasks/line_annotation_task_spec.rb @@ -0,0 +1,20 @@ +RSpec.describe Scale::Api::Tasks, 'line annotation' do + let(:scale) { Scale.new(api_key: SCALE_TEST_API_KEY) } + + it 'returns annotations for line lineannotation task' do + response = VCR.use_cassette('line_annotation') do + scale.create_lineannotation_task( + callback_url: 'http://www.example.com/callback', + instruction: 'Draw a tight shape around the big cow', + attachment_type: 'image', + attachment: 'http://i.imgur.com/v4cBreD.jpg', + objects_to_annotate: ['big cow'], + with_labels: true) + end + + expect(response.type).to eq 'lineannotation' + response.response['annotations'].each do |annotation| + expect(annotation.keys).to contain_exactly('vertices', "label") + end + end +end diff --git a/spec/lib/api/tasks/point_annotation_task_spec.rb b/spec/lib/api/tasks/point_annotation_task_spec.rb new file mode 100644 index 0000000..57c3ea4 --- /dev/null +++ b/spec/lib/api/tasks/point_annotation_task_spec.rb @@ -0,0 +1,17 @@ +RSpec.describe Scale::Api::Tasks, 'point annotation' do + let(:scale) { Scale.new(api_key: SCALE_TEST_API_KEY) } + + it 'returns annotations for point annotation task' do + response = VCR.use_cassette('point_annotation') do + scale.create_pointannotation_task( + callback_url: 'http://www.example.com/callback', + instruction: 'Draw a point on every **headlight** and **brakelight** of a car in the image.', + attachment_type: 'image', + attachment: 'http://i.imgur.com/XOJbalC.jpg', + objects_to_annotate: ['headlight', 'brakelight'], + with_labels: true) + end + + expect(response.type).to eq 'pointannotation' + end +end diff --git a/spec/lib/api/tasks/polygon_annotation_task_spec.rb b/spec/lib/api/tasks/polygon_annotation_task_spec.rb new file mode 100644 index 0000000..f51b131 --- /dev/null +++ b/spec/lib/api/tasks/polygon_annotation_task_spec.rb @@ -0,0 +1,20 @@ +RSpec.describe Scale::Api::Tasks, 'polygon annotation' do + let(:scale) { Scale.new(api_key: SCALE_TEST_API_KEY) } + + it 'returns annotations for polygon annotation task' do + response = VCR.use_cassette('polygon_annotation') do + scale.create_polygonannotation_task( + callback_url: 'http://www.example.com/callback', + instruction: 'Draw a tight shape around the big cow', + attachment_type: 'image', + attachment: 'http://i.imgur.com/v4cBreD.jpg', + objects_to_annotate: ['big cow'], + with_labels: true) + end + + expect(response.type).to eq 'polygonannotation' + response.response['annotations'].each do |annotation| + expect(annotation.keys).to contain_exactly('vertices', "label") + end + end +end diff --git a/spec/lib/api/tasks/segment_annotation_task_spec.rb b/spec/lib/api/tasks/segment_annotation_task_spec.rb new file mode 100644 index 0000000..ac5267e --- /dev/null +++ b/spec/lib/api/tasks/segment_annotation_task_spec.rb @@ -0,0 +1,22 @@ +RSpec.describe Scale::Api::Tasks, 'segment annotation' do + let(:scale) { Scale.new(api_key: SCALE_TEST_API_KEY) } + + it 'returns annotations for segment annotation task' do + response = VCR.use_cassette('segment_annotation') do + scale.create_segmentannotation_task({ + callback_url: 'http://www.example.com/callback', + instruction: 'Please segment the image using the given labels.', + attachment_type: 'image', + attachment: 'http://i.imgur.com/XOJbalC.jpg', + labels: ['background', 'road', 'vegetation', 'lane marking'], + instance_labels: ['vehicle', 'pedestrian'], + allow_unlabeled: false}) + end + + expect(response.type).to eq 'segmentannotation' + expect(response.response['annotations'].keys) + .to contain_exactly('unlabeled', 'labeled', 'combined') + expect(response.response['labelMapping'].keys) + .to contain_exactly('background', 'road', 'vegetation', 'lane marking') + end +end diff --git a/spec/lib/api/tasks/transcription_task_spec.rb b/spec/lib/api/tasks/transcription_task_spec.rb new file mode 100644 index 0000000..8112303 --- /dev/null +++ b/spec/lib/api/tasks/transcription_task_spec.rb @@ -0,0 +1,20 @@ +RSpec.describe Scale::Api::Tasks, 'transcription' do + let(:scale) { Scale.new(api_key: SCALE_TEST_API_KEY) } + + it 'returns transcription for transcription task' do + response = VCR.use_cassette('transcription') do + scale.create_transcription_task( + callback_url: 'http://www.example.com/callback', + instruction: 'Transcribe the given fields.', + attachment_type: 'website', + attachment: 'http://news.ycombinator.com/', + fields: { + title: 'Title of Webpage', + top_result: 'Title of the top result' + }) + end + + expect(response.type).to eq 'transcription' + expect(response.response['fields'].keys).to contain_exactly('title', 'top_result') + end +end diff --git a/spec/lib/api/tasks_spec.rb b/spec/lib/api/tasks_spec.rb new file mode 100644 index 0000000..c14a326 --- /dev/null +++ b/spec/lib/api/tasks_spec.rb @@ -0,0 +1,64 @@ +RSpec.describe Scale::Api::Tasks do + let(:scale) { Scale.new(api_key: SCALE_TEST_API_KEY) } + let(:original_task) do + # reuse cassette from categorization_task_spec + VCR.use_cassette('categorization') do + scale.create_categorization_task( + callback_url: 'http://www.example.com/callback', + instruction: 'Is this company public or private?', + attachment_type: 'website', + attachment: 'https://www.google.com', + categories: %w(public private)) + end + end + + describe '#find' do + context 'with valid task id' do + it 'fetches the same task' do + retrieved_task = VCR.use_cassette('task_find_valid') do + scale.tasks.find(original_task.id) + end + + expect(retrieved_task.id).to eq original_task.id + expect(retrieved_task.type).to eq original_task.type + expect(retrieved_task.callback_url).to eq original_task.callback_url + expect(retrieved_task.instruction).to eq original_task.instruction + expect(retrieved_task.params).to eq original_task.params + expect(retrieved_task.metadata).to eq original_task.metadata + expect(retrieved_task.created_at).to eq original_task.created_at + end + end + + context 'with invalid task id' do + it 'raises error' do + expect { + VCR.use_cassette('task_find_invalid') do + scale.tasks.find('invalid_task_id') + end + }.to raise_error Scale::Api::NotFound + end + end + end + + describe '#cancel' do + context 'with valid task id' do + it 'cancels the task' do + canceled_task = VCR.use_cassette('task_cancel_valid') do + scale.tasks.cancel(original_task.id) + end + + expect(canceled_task.canceled?).to be true + end + end + + context 'with invalid task id' do + it 'raises error' do + expect { + VCR.use_cassette('task_cancel_invalid') do + scale.tasks.cancel('invalid_task_id') + end + }.to raise_error Scale::Api::NotFound + end + end + end +end diff --git a/spec/lib/scale_spec.rb b/spec/lib/scale_spec.rb new file mode 100644 index 0000000..cc6c24e --- /dev/null +++ b/spec/lib/scale_spec.rb @@ -0,0 +1,52 @@ +require 'securerandom' +require 'scale/api' +require 'scale/api/errors' + +RSpec.describe Scale do + describe '.validate_api_key' do + it 'rejects short api key' do + expect{ + Scale.validate_api_key(SecureRandom.base64(4)) + }.to raise_error Scale::Api::APIKeyInvalid + end + + it 'accepts test key' do + expect{ + Scale.validate_api_key("test_#{SecureRandom.base64(4)}") + }.to_not raise_error + end + + it 'accepts live key' do + expect{ + Scale.validate_api_key("live_#{SecureRandom.base64(4)}") + }.to_not raise_error + end + end + + let(:test_client) { Scale.new(api_key: "test_#{SecureRandom.base64(10)}") } + let(:live_client) { Scale.new(api_key: "live_#{SecureRandom.base64(10)}") } + + describe '#test?' do + it 'recognizes test api' do + expect(test_client.test?).to be true + expect(test_client.test).to be true + expect(test_client.test_mode?).to be true + + expect(live_client.test?).to be false + expect(live_client.test).to be false + expect(live_client.test_mode?).to be false + end + end + + describe '#live?' do + it 'recognizes live api' do + expect(test_client.live?).to be false + expect(test_client.live).to be false + expect(test_client.live_mode?).to be false + + expect(live_client.live?).to be true + expect(live_client.live).to be true + expect(live_client.live_mode?).to be true + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..230ef1c --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,26 @@ +require 'rspec' +require 'webmock/rspec' +require 'vcr' +require 'scale' +require 'cassettes/custom_serializer' + +# by default, rspec use local vcr cassettes to mock client - server +# communication; to run tests against production server (or update +# cassettes), set SCALE_TEST_API_KEY, and run rspec with +# SCALE_TEST_SERVER=production, which is encapsulated in rake task +# spec:production +SCALE_TEST_API_KEY = ENV['SCALE_TEST_API_KEY'] || 'test_api_key' +SCALE_TEST_SERVER = ENV['SCALE_TEST_SERVER'] || 'local' + +RSpec.configure do |config| + config.include WebMock::API +end + +VCR.configure do |c| + c.cassette_library_dir = 'spec/cassettes' + c.hook_into :webmock + c.cassette_serializers[:yaml] = Cassettes::CustomSerializer + c.default_cassette_options = { + record: SCALE_TEST_SERVER == 'production' ? :all : :once + } +end diff --git a/tasks/spec_production.rake b/tasks/spec_production.rake new file mode 100644 index 0000000..07bffbc --- /dev/null +++ b/tasks/spec_production.rake @@ -0,0 +1,10 @@ +namespace :spec do + desc 'Run rspec against production server instead of local cassettes' + task :production do + if ENV['SCALE_TEST_API_KEY'].nil? + raise 'Please specify SCALE_TEST_API_KEY to run against production server' + end + ENV['SCALE_TEST_SERVER'] = 'production' + Rake::Task['spec'].invoke + end +end