crowdkit 0.1.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 +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +97 -0
- data/LICENSE.txt +20 -0
- data/README.md +178 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/crowdkit.gemspec +122 -0
- data/lib/crowdkit.rb +77 -0
- data/lib/crowdkit/api.rb +171 -0
- data/lib/crowdkit/api/arguments.rb +187 -0
- data/lib/crowdkit/api/factory.rb +29 -0
- data/lib/crowdkit/api/request_methods.rb +70 -0
- data/lib/crowdkit/api/response_wrapper.rb +122 -0
- data/lib/crowdkit/client.rb +36 -0
- data/lib/crowdkit/client/account.rb +7 -0
- data/lib/crowdkit/client/jobs.rb +56 -0
- data/lib/crowdkit/client/judgments.rb +9 -0
- data/lib/crowdkit/client/poll.rb +48 -0
- data/lib/crowdkit/client/statuses.rb +14 -0
- data/lib/crowdkit/client/units.rb +54 -0
- data/lib/crowdkit/client/workers.rb +13 -0
- data/lib/crowdkit/client/worksets.rb +27 -0
- data/lib/crowdkit/config.rb +62 -0
- data/lib/crowdkit/core_ext/array.rb +7 -0
- data/lib/crowdkit/core_ext/hash.rb +30 -0
- data/lib/crowdkit/core_ext/sawyer.rb +7 -0
- data/lib/crowdkit/error.rb +198 -0
- data/lib/crowdkit/middleware/raise_error.rb +14 -0
- data/lib/crowdkit/ssl_certs/cacerts.pem +3868 -0
- data/lib/crowdkit/version.rb +3 -0
- data/spec/crowdkit/client/account_spec.rb +19 -0
- data/spec/crowdkit/client/jobs_spec.rb +275 -0
- data/spec/crowdkit/client/judgments_spec.rb +30 -0
- data/spec/crowdkit/client/statuses_spec.rb +30 -0
- data/spec/crowdkit/client/units_spec.rb +266 -0
- data/spec/crowdkit/client/workers_spec.rb +33 -0
- data/spec/crowdkit/client/worksets_spec.rb +113 -0
- data/spec/crowdkit/client_spec.rb +55 -0
- data/spec/crowdkit_spec.rb +139 -0
- data/spec/fixtures/account.json +7 -0
- data/spec/fixtures/jobs/copy.json +79 -0
- data/spec/fixtures/jobs/job.json +1 -0
- data/spec/fixtures/jobs/order.json +53 -0
- data/spec/fixtures/jobs/search.json +1 -0
- data/spec/fixtures/judgments/index.json +46 -0
- data/spec/fixtures/judgments/show.json +16 -0
- data/spec/fixtures/root.json +1 -0
- data/spec/fixtures/statuses/index.json +25 -0
- data/spec/fixtures/statuses/status.json +12 -0
- data/spec/fixtures/units/copy.json +34 -0
- data/spec/fixtures/units/index.json +1502 -0
- data/spec/fixtures/units/poll/normal.json +902 -0
- data/spec/fixtures/units/poll/small.json +602 -0
- data/spec/fixtures/units/show.json +34 -0
- data/spec/fixtures/upload.csv +25 -0
- data/spec/fixtures/worksets/index.json +20 -0
- data/spec/fixtures/worksets/show.json +9 -0
- data/spec/spec_helper.rb +53 -0
- data/spec/support/base.rb +11 -0
- metadata +218 -0
data/lib/crowdkit.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'sawyer'
|
2
|
+
require 'crowdkit/version'
|
3
|
+
require 'crowdkit/core_ext/hash'
|
4
|
+
require 'crowdkit/core_ext/array'
|
5
|
+
require 'crowdkit/core_ext/sawyer'
|
6
|
+
require 'crowdkit/config'
|
7
|
+
require 'crowdkit/api'
|
8
|
+
require 'crowdkit/client'
|
9
|
+
require 'crowdkit/error'
|
10
|
+
require 'crowdkit/middleware/raise_error'
|
11
|
+
|
12
|
+
module Crowdkit
|
13
|
+
def self.new(overrides={}, &block)
|
14
|
+
#Use the global config if no overrides present
|
15
|
+
if @configuration && overrides.empty? && !block_given?
|
16
|
+
Client.new(@configuration)
|
17
|
+
else
|
18
|
+
Client.new(overrides, &block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.last_response
|
23
|
+
@last_response
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.last_response=(last_response)
|
27
|
+
@last_response = last_response
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.configure(overrides={}, &block)
|
31
|
+
@configuration = Config.new(overrides, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.reset!
|
35
|
+
@last_response = nil
|
36
|
+
@configuration = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# This takes a response from a resource that processes something in the
|
40
|
+
# background (i.e. a 202 response) and will ping the status API until it timesout
|
41
|
+
# an error is returned, or the background process is completed.
|
42
|
+
def self.wait_on_status(resource, max_time_to_wait_in_seconds = 30)
|
43
|
+
raise ArgumentError, "This resource did not respond with a 202 status code and has no status to wait on." unless resource.last_response.status == 202
|
44
|
+
progressbar = ProgressBar.create(total: 100)
|
45
|
+
status = resource.current_status
|
46
|
+
wait = 1
|
47
|
+
|
48
|
+
max_time_to_wait_in_seconds.times do |i|
|
49
|
+
break if status.state == "completed"
|
50
|
+
raise StatusError.new(status) if ["failed", "killed"].include?(status.state)
|
51
|
+
|
52
|
+
#increase wait time to 5 seconds after 5 seconds
|
53
|
+
wait = 5 if i == 5
|
54
|
+
#increase wait time to 10 seconds after 30 seconds
|
55
|
+
wait = 10 if i == 30
|
56
|
+
|
57
|
+
if i % wait == 0
|
58
|
+
percents = status.percent_complete.to_i
|
59
|
+
progressbar.progress = percents if percents > 0
|
60
|
+
sleep(wait)
|
61
|
+
status = resource.current_status
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
raise StatusError.new(status, true) unless status.state == "completed"
|
66
|
+
progressbar.finish
|
67
|
+
status
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
#Not convinced we even want to support this...
|
73
|
+
def self.method_missing(method_name, *args, &block)
|
74
|
+
return super unless new.respond_to?(method_name)
|
75
|
+
new.send(method_name, *args, &block)
|
76
|
+
end
|
77
|
+
end
|
data/lib/crowdkit/api.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'crowdkit/api/arguments'
|
2
|
+
require 'crowdkit/api/factory'
|
3
|
+
require 'crowdkit/api/request_methods'
|
4
|
+
require 'crowdkit/api/response_wrapper'
|
5
|
+
|
6
|
+
module Crowdkit
|
7
|
+
class API
|
8
|
+
extend Forwardable
|
9
|
+
include RequestMethods
|
10
|
+
attr_accessor :stored_params
|
11
|
+
attr_reader :config, :client
|
12
|
+
attr_writer :auto_pagination
|
13
|
+
|
14
|
+
#We delegate last_response to the attr_accessor of the top level Client
|
15
|
+
def_delegators :client, :last_response, :last_response=
|
16
|
+
|
17
|
+
#This should only be called by API Factory to ensure _config, and _client
|
18
|
+
#get set. We override this in Client as it's the root of the API.
|
19
|
+
def initialize(options, &block)
|
20
|
+
@config = options.delete(:_config)
|
21
|
+
@client = options.delete(:_client)
|
22
|
+
@stored_params = options
|
23
|
+
with(options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def auto_pagination
|
27
|
+
@auto_pagination || @config.auto_paginate
|
28
|
+
end
|
29
|
+
|
30
|
+
# Acts as setter and getter for api requests arguments parsing.
|
31
|
+
#
|
32
|
+
# Returns Arguments instance.
|
33
|
+
#
|
34
|
+
def arguments(args=(not_set = true), options={}, &block)
|
35
|
+
if not_set
|
36
|
+
@arguments
|
37
|
+
else
|
38
|
+
@arguments = Arguments.new(self, options.symbolize_keys).parse(*args, &block)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Set an option to a given value
|
43
|
+
#
|
44
|
+
# @param [String] option
|
45
|
+
# @param [Object] value
|
46
|
+
# @param [Boolean] ignore_setter
|
47
|
+
#
|
48
|
+
# @return [self]
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def set(option, value=(not_set=true), ignore_setter=false, &block)
|
52
|
+
raise ArgumentError, 'value not set' if block and !not_set
|
53
|
+
return self if !not_set and value.nil?
|
54
|
+
|
55
|
+
if not_set
|
56
|
+
set_options option
|
57
|
+
return self
|
58
|
+
end
|
59
|
+
|
60
|
+
if respond_to?("#{option}=") and not ignore_setter
|
61
|
+
return __send__("#{option}=", value)
|
62
|
+
end
|
63
|
+
|
64
|
+
define_accessors option, value
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
#overridden in children API's that have their own id's
|
69
|
+
def id_key
|
70
|
+
:job_id
|
71
|
+
end
|
72
|
+
|
73
|
+
# Scope for passing request required arguments.
|
74
|
+
#
|
75
|
+
def with(args)
|
76
|
+
case args
|
77
|
+
when Hash
|
78
|
+
#This happens when the job id is passed early in the chain
|
79
|
+
if (custom = args.delete(:_args)) && custom.first
|
80
|
+
stored_params[id_key] = custom.first
|
81
|
+
set id_key, custom.first
|
82
|
+
end
|
83
|
+
set args
|
84
|
+
when Fixnum, /^(\d+|([a-z])([a-z]|\d)*)$/
|
85
|
+
stored_params[id_key] = args
|
86
|
+
set id_key, args
|
87
|
+
else
|
88
|
+
::Kernel.raise ArgumentError, 'This api does not support passed in arguments'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Defines a namespace
|
93
|
+
#
|
94
|
+
# @param [Array[Symbol]] names
|
95
|
+
# the name for the scope
|
96
|
+
#
|
97
|
+
# @return [self]
|
98
|
+
#
|
99
|
+
# @api public
|
100
|
+
def self.namespace(*names)
|
101
|
+
options = names.last.is_a?(Hash) ? names.pop : {}
|
102
|
+
names = names.map(&:to_sym)
|
103
|
+
name = names.pop
|
104
|
+
return if public_method_defined?(name)
|
105
|
+
|
106
|
+
class_name = extract_class_name(name, options)
|
107
|
+
define_method(name) do |*args, &block|
|
108
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
109
|
+
options[:_args] = args
|
110
|
+
options[:_config] = @config
|
111
|
+
options[:_client] = @client
|
112
|
+
API::Factory.new(class_name, stored_params.merge(options), &block)
|
113
|
+
end
|
114
|
+
self
|
115
|
+
end
|
116
|
+
|
117
|
+
# Extracts class name from options
|
118
|
+
#
|
119
|
+
# @return [String]
|
120
|
+
#
|
121
|
+
# @api private
|
122
|
+
def self.extract_class_name(name, options)
|
123
|
+
if !options[:class_name]
|
124
|
+
converted = options.fetch(:full_name, name).to_s
|
125
|
+
converted = converted.split('_').map(&:capitalize).join
|
126
|
+
class_name = options.fetch(:root, false) ? '': "#{self.name}::"
|
127
|
+
class_name += converted
|
128
|
+
class_name
|
129
|
+
else
|
130
|
+
options[:class_name]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
# Set multiple options
|
137
|
+
#
|
138
|
+
# @api private
|
139
|
+
def set_options(options)
|
140
|
+
unless options.respond_to?(:each)
|
141
|
+
raise ArgumentError, 'cannot iterate over value'
|
142
|
+
end
|
143
|
+
options.each { |key, value| set(key, value) }
|
144
|
+
end
|
145
|
+
|
146
|
+
# Define setters and getters
|
147
|
+
#
|
148
|
+
# @api private
|
149
|
+
def define_accessors(option, value)
|
150
|
+
setter = proc { |val| set option, val, true }
|
151
|
+
getter = proc { value }
|
152
|
+
|
153
|
+
define_singleton_method("#{option}=", setter) if setter
|
154
|
+
define_singleton_method(option, getter) if getter
|
155
|
+
end
|
156
|
+
|
157
|
+
# Dynamically define a method for setting request option
|
158
|
+
#
|
159
|
+
# @api private
|
160
|
+
def define_singleton_method(method_name, content=Proc.new)
|
161
|
+
(class << self; self; end).class_eval do
|
162
|
+
undef_method(method_name) if method_defined?(method_name)
|
163
|
+
if String === content
|
164
|
+
class_eval("def #{method_name}() #{content}; end")
|
165
|
+
else
|
166
|
+
define_method(method_name, &content)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end # API
|
171
|
+
end # Crowdkit
|
@@ -0,0 +1,187 @@
|
|
1
|
+
module Crowdkit
|
2
|
+
class API
|
3
|
+
# Request arguments handler
|
4
|
+
class Arguments
|
5
|
+
AUTO_PAGINATION = :auto_pagination
|
6
|
+
|
7
|
+
# Parameters passed to request
|
8
|
+
attr_reader :params
|
9
|
+
|
10
|
+
attr_reader :remaining
|
11
|
+
|
12
|
+
# The request api
|
13
|
+
#
|
14
|
+
attr_reader :api
|
15
|
+
|
16
|
+
# Required arguments
|
17
|
+
#
|
18
|
+
attr_reader :required
|
19
|
+
private :required
|
20
|
+
|
21
|
+
# Optional arguments
|
22
|
+
#
|
23
|
+
attr_reader :optional
|
24
|
+
private :optional
|
25
|
+
|
26
|
+
# Takes api, filters and required arguments
|
27
|
+
#
|
28
|
+
# = Parameters
|
29
|
+
# :required - arguments that must be present before request is fired
|
30
|
+
#
|
31
|
+
def initialize(api, options={})
|
32
|
+
@api = api
|
33
|
+
@required = options.fetch(:required, []).map(&:to_s)
|
34
|
+
@optional = options.fetch(:optional, []).map(&:to_s)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Parse arguments to allow for flexible api calls.
|
38
|
+
# Arguments can be part of parameters hash or be simple string arguments.
|
39
|
+
#
|
40
|
+
def parse(*args, &block)
|
41
|
+
options = args.extract_options!
|
42
|
+
|
43
|
+
if !args.empty? && ![File, Array].include?(args.last.class)
|
44
|
+
parse_arguments *args
|
45
|
+
else
|
46
|
+
# Arguments are inside the parameters hash
|
47
|
+
parse_options options
|
48
|
+
end
|
49
|
+
|
50
|
+
@params = options
|
51
|
+
@remaining = extract_remaining(args)
|
52
|
+
extract_pagination(options)
|
53
|
+
yield_or_eval(&block)
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
# Check if required keys are present inside parameters hash.
|
58
|
+
#
|
59
|
+
def assert_required(required)
|
60
|
+
assert_required_keys required, params
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
# Check if parameters match expected values.
|
65
|
+
#
|
66
|
+
def assert_values(values, key=nil)
|
67
|
+
assert_valid_values values, (key.nil? ? params : params[key])
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Check and set all requried arguments.
|
74
|
+
#
|
75
|
+
def parse_arguments(*args)
|
76
|
+
assert_presence_of *args
|
77
|
+
required.each_with_index do |req, indx|
|
78
|
+
api.set req, args[indx]
|
79
|
+
end
|
80
|
+
check_requirement!(*args)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Find remaining arguments
|
84
|
+
#
|
85
|
+
def extract_remaining(args)
|
86
|
+
args[required.size..-1]
|
87
|
+
end
|
88
|
+
|
89
|
+
# Fine auto_pagination parameter in options hash
|
90
|
+
#
|
91
|
+
def extract_pagination(options)
|
92
|
+
if (value = options.delete(AUTO_PAGINATION))
|
93
|
+
api.client.auto_pagination = value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Remove required arguments from parameters and
|
98
|
+
# validate their presence(if not nil or empty string).
|
99
|
+
#
|
100
|
+
def parse_options(options)
|
101
|
+
options.each { |key, val| remove_required(options, key, val) }
|
102
|
+
provided_args = check_assignment!(options)
|
103
|
+
check_requirement!(*provided_args.keys)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Remove required argument from parameters
|
107
|
+
#
|
108
|
+
def remove_required(options, key, val)
|
109
|
+
key = key.to_s
|
110
|
+
if required.include? key
|
111
|
+
assert_presence_of val
|
112
|
+
options.delete key.intern
|
113
|
+
api.set key, val
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Check if required arguments have been set on instance.
|
118
|
+
#
|
119
|
+
def check_assignment!(options)
|
120
|
+
result = required.inject({}) { |hash, arg|
|
121
|
+
if api.respond_to?(:"#{arg}")
|
122
|
+
hash[arg] = api.send(:"#{arg}")
|
123
|
+
else
|
124
|
+
hash[arg] = nil
|
125
|
+
end
|
126
|
+
hash
|
127
|
+
}
|
128
|
+
assert_presence_of result
|
129
|
+
result
|
130
|
+
end
|
131
|
+
|
132
|
+
# Check if required arguments are present.
|
133
|
+
#
|
134
|
+
def check_requirement!(*args)
|
135
|
+
args_length = args.length
|
136
|
+
required_length = required.length
|
137
|
+
|
138
|
+
if args_length < required_length
|
139
|
+
::Kernel.raise ArgumentError, "wrong number of arguments (#{args_length} for #{required_length})"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Ensure that esential arguments are present before request is made.
|
144
|
+
#
|
145
|
+
# == Parameters
|
146
|
+
# Hash/Array of arguments to be checked against nil and empty string
|
147
|
+
#
|
148
|
+
# == Example
|
149
|
+
# assert_presence_of user: '...', repo: '...'
|
150
|
+
# assert_presence_of user, repo
|
151
|
+
#
|
152
|
+
def assert_presence_of(*args)
|
153
|
+
hash = args.last.is_a?(::Hash) ? args.pop : {}
|
154
|
+
|
155
|
+
errors = hash.select { |key, val| val.to_s.empty? }
|
156
|
+
raise Crowdkit::ValidationError.new(errors) unless errors.empty?
|
157
|
+
|
158
|
+
args.each do |arg|
|
159
|
+
raise ArgumentError, "parameter cannot be nil" if arg.nil?
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Validate all keys present in a provided hash against required set,
|
164
|
+
# on mismatch raise CrowdKit::Error::RequiredParams
|
165
|
+
# Note that keys need to be in the same format i.e. symbols or strings,
|
166
|
+
# otherwise the comparison will fail.
|
167
|
+
#
|
168
|
+
def assert_required_keys(required, provided)
|
169
|
+
result = required.all? do |key|
|
170
|
+
provided.deep_key? key
|
171
|
+
end
|
172
|
+
if !result
|
173
|
+
raise CrowdKit::RequiredParams.new(provided, required)
|
174
|
+
end
|
175
|
+
result
|
176
|
+
end
|
177
|
+
|
178
|
+
# Evaluate block
|
179
|
+
#
|
180
|
+
def yield_or_eval(&block)
|
181
|
+
return unless block
|
182
|
+
block.arity > 0 ? yield(self) : self.instance_eval(&block)
|
183
|
+
end
|
184
|
+
|
185
|
+
end # Arguments
|
186
|
+
end # Api
|
187
|
+
end # Crowdkit
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Crowdkit
|
2
|
+
class API
|
3
|
+
class Factory
|
4
|
+
|
5
|
+
# Instantiates a new CrowdFlower api object
|
6
|
+
#
|
7
|
+
def self.new(klass, options={}, &block)
|
8
|
+
return create_instance(klass, options, &block) if klass
|
9
|
+
raise ArgumentError, 'must provide API class to be instantiated'
|
10
|
+
end
|
11
|
+
|
12
|
+
# Passes configuration options to instantiated class
|
13
|
+
#
|
14
|
+
def self.create_instance(klass, options, &block)
|
15
|
+
options.symbolize_keys!
|
16
|
+
convert_to_constant(klass.to_s).new options, &block
|
17
|
+
end
|
18
|
+
|
19
|
+
# Convert name to constant
|
20
|
+
#
|
21
|
+
def self.convert_to_constant(classes)
|
22
|
+
classes.split('::').inject(Crowdkit) do |constant, klass|
|
23
|
+
constant.const_get klass
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end # Factory
|
28
|
+
end # Api
|
29
|
+
end # Crowdkit
|