dbgrandi-ruby-aws 1.2.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.
Files changed (70) hide show
  1. data/History.txt +38 -0
  2. data/LICENSE.txt +202 -0
  3. data/Manifest.txt +69 -0
  4. data/NOTICE.txt +4 -0
  5. data/README.txt +105 -0
  6. data/Rakefile +20 -0
  7. data/bin/ruby-aws +9 -0
  8. data/lib/amazon/util.rb +10 -0
  9. data/lib/amazon/util/binder.rb +44 -0
  10. data/lib/amazon/util/data_reader.rb +157 -0
  11. data/lib/amazon/util/filter_chain.rb +79 -0
  12. data/lib/amazon/util/hash_nesting.rb +93 -0
  13. data/lib/amazon/util/lazy_results.rb +59 -0
  14. data/lib/amazon/util/logging.rb +23 -0
  15. data/lib/amazon/util/paginated_iterator.rb +70 -0
  16. data/lib/amazon/util/proactive_results.rb +116 -0
  17. data/lib/amazon/util/threadpool.rb +129 -0
  18. data/lib/amazon/util/user_data_store.rb +100 -0
  19. data/lib/amazon/webservices/mechanical_turk.rb +117 -0
  20. data/lib/amazon/webservices/mechanical_turk_requester.rb +261 -0
  21. data/lib/amazon/webservices/mturk/mechanical_turk_error_handler.rb +136 -0
  22. data/lib/amazon/webservices/mturk/question_generator.rb +58 -0
  23. data/lib/amazon/webservices/util/amazon_authentication_relay.rb +64 -0
  24. data/lib/amazon/webservices/util/command_line.rb +156 -0
  25. data/lib/amazon/webservices/util/convenience_wrapper.rb +90 -0
  26. data/lib/amazon/webservices/util/filter_proxy.rb +45 -0
  27. data/lib/amazon/webservices/util/mock_transport.rb +70 -0
  28. data/lib/amazon/webservices/util/request_signer.rb +42 -0
  29. data/lib/amazon/webservices/util/rest_transport.rb +108 -0
  30. data/lib/amazon/webservices/util/soap_simplifier.rb +48 -0
  31. data/lib/amazon/webservices/util/soap_transport.rb +38 -0
  32. data/lib/amazon/webservices/util/soap_transport_header_handler.rb +27 -0
  33. data/lib/amazon/webservices/util/unknown_result_exception.rb +27 -0
  34. data/lib/amazon/webservices/util/validation_exception.rb +55 -0
  35. data/lib/amazon/webservices/util/xml_simplifier.rb +61 -0
  36. data/lib/ruby-aws.rb +21 -0
  37. data/lib/ruby-aws/version.rb +8 -0
  38. data/samples/mturk/best_image/BestImage.rb +61 -0
  39. data/samples/mturk/best_image/best_image.properties +39 -0
  40. data/samples/mturk/best_image/best_image.question +82 -0
  41. data/samples/mturk/blank_slate/BlankSlate.rb +63 -0
  42. data/samples/mturk/blank_slate/BlankSlate_multithreaded.rb +67 -0
  43. data/samples/mturk/helloworld/MTurkHelloWorld.rb +56 -0
  44. data/samples/mturk/helloworld/mturk.yml +8 -0
  45. data/samples/mturk/reviewer/Reviewer.rb +103 -0
  46. data/samples/mturk/reviewer/mturk.yml +8 -0
  47. data/samples/mturk/simple_survey/SimpleSurvey.rb +90 -0
  48. data/samples/mturk/simple_survey/simple_survey.question +30 -0
  49. data/samples/mturk/site_category/SiteCategory.rb +87 -0
  50. data/samples/mturk/site_category/externalpage.htm +71 -0
  51. data/samples/mturk/site_category/site_category.input +6 -0
  52. data/samples/mturk/site_category/site_category.properties +45 -0
  53. data/samples/mturk/site_category/site_category.question +9 -0
  54. data/test/mturk/test_changehittypeofhit.rb +130 -0
  55. data/test/mturk/test_error_handler.rb +135 -0
  56. data/test/mturk/test_mechanical_turk_requester.rb +178 -0
  57. data/test/mturk/test_mock_mechanical_turk_requester.rb +205 -0
  58. data/test/test_ruby-aws.rb +22 -0
  59. data/test/unit/test_binder.rb +89 -0
  60. data/test/unit/test_data_reader.rb +135 -0
  61. data/test/unit/test_exceptions.rb +32 -0
  62. data/test/unit/test_hash_nesting.rb +93 -0
  63. data/test/unit/test_lazy_results.rb +89 -0
  64. data/test/unit/test_mock_transport.rb +132 -0
  65. data/test/unit/test_paginated_iterator.rb +58 -0
  66. data/test/unit/test_proactive_results.rb +108 -0
  67. data/test/unit/test_question_generator.rb +54 -0
  68. data/test/unit/test_threadpool.rb +50 -0
  69. data/test/unit/test_user_data_store.rb +80 -0
  70. metadata +158 -0
@@ -0,0 +1,261 @@
1
+ # Copyright:: Copyright (c) 2007 Amazon Technologies, Inc.
2
+ # License:: Apache License, Version 2.0
3
+
4
+ require 'erb'
5
+ require 'monitor'
6
+ require 'amazon/util'
7
+ require 'amazon/webservices/util/xml_simplifier'
8
+ require 'amazon/webservices/util/convenience_wrapper'
9
+ require 'amazon/webservices/mechanical_turk'
10
+
11
+ module Amazon
12
+ module WebServices
13
+
14
+ class MechanicalTurkRequester < Amazon::WebServices::Util::ConvenienceWrapper
15
+
16
+ WSDL_VERSION = "2007-06-21"
17
+
18
+ ABANDONMENT_RATE_QUALIFICATION_TYPE_ID = "00000000000000000070";
19
+ APPROVAL_RATE_QUALIFICATION_TYPE_ID = "000000000000000000L0";
20
+ REJECTION_RATE_QUALIFICATION_TYPE_ID = "000000000000000000S0";
21
+ RETURN_RATE_QUALIFICATION_TYPE_ID = "000000000000000000E0";
22
+ SUBMISSION_RATE_QUALIFICATION_TYPE_ID = "00000000000000000000";
23
+ LOCALE_QUALIFICATION_TYPE_ID = "00000000000000000071";
24
+
25
+ DEFAULT_THREADCOUNT = 10
26
+
27
+ serviceCall :RegisterHITType, :RegisterHITTypeResult, {
28
+ :AssignmentDurationInSeconds => 60*60,
29
+ :AutoApprovalDelayInSeconds => 60*60*24*7
30
+ }
31
+
32
+ serviceCall :CreateHIT, :HIT, { :MaxAssignments => 1,
33
+ :AssignmentDurationInSeconds => 60*60,
34
+ :AutoApprovalDelayInSeconds => 60*60*24*7,
35
+ :LifetimeInSeconds => 60*60*24,
36
+ }
37
+
38
+ serviceCall :DisableHIT, :DisableHITResult
39
+ serviceCall :DisposeHIT, :DisposeHITResult
40
+ serviceCall :ExtendHIT, :ExtendHITResult
41
+ serviceCall :ForceExpireHIT, :ForceExpireHITResult
42
+ serviceCall :GetHIT, :HIT, { :ResponseGroup => %w( Minimal HITDetail HITQuestion HITAssignmentSummary ) }
43
+ serviceCall :ChangeHITTypeOfHIT, :ChangeHITTypeOfHITResult
44
+
45
+ serviceCall :SearchHITs, :SearchHITsResult
46
+ serviceCall :GetReviewableHITs, :GetReviewableHITsResult
47
+ serviceCall :SetHITAsReviewing, :SetHITAsReviewingResult
48
+ serviceCall :GetAssignmentsForHIT, :GetAssignmentsForHITResult
49
+ serviceCall :ApproveAssignment, :ApproveAssignmentResult
50
+ serviceCall :RejectAssignment, :RejectAssignmentResult
51
+
52
+ paginate :SearchHITs, :HIT
53
+ paginate :GetReviewableHITs, :HIT
54
+ paginate :GetAssignmentsForHIT, :Assignment
55
+
56
+ serviceCall :GrantBonus, :GrantBonusResult
57
+ serviceCall :GetBonusPayments, :GetBonusPaymentsResult
58
+
59
+ serviceCall :CreateQualificationType, :QualificationType, { :QualificationTypeStatus => 'Active' }
60
+ serviceCall :GetQualificationType, :QualificationType
61
+ serviceCall :SearchQualificationTypes, :SearchQualificationTypesResult, { :MustBeRequestable => true }
62
+ serviceCall :UpdateQualificationType, :QualificationType
63
+ serviceCall :GetQualificationsForQualificationType, :GetQualificationsForQualificationTypeResult, { :Status => 'Granted' }
64
+ serviceCall :GetHITsForQualificationType, :GetHITsForQualificationTypeResult
65
+
66
+ paginate :SearchQualificationTypes, :QualificationType
67
+ paginate :GetQualificationsForQualificationType, :Qualification
68
+
69
+ serviceCall :AssignQualification, :AssignQualificationResult
70
+ serviceCall :GetQualificationRequests, :GetQualificationRequestsResult
71
+ serviceCall :GrantQualification, :GrantQualificationResult
72
+ serviceCall :RejectQualificationRequest, :RejectQualificationRequestResult
73
+ serviceCall :GetQualificationScore, :Qualification
74
+ serviceCall :UpdateQualificationScore, :UpdateQualificationScoreResult
75
+ serviceCall :RevokeQualification, :RevokeQualificationResult
76
+
77
+ paginate :GetQualificationRequests, :QualificationRequest
78
+
79
+ serviceCall :SetHITTypeNotification, :SetHITTypeNotificationResult
80
+ serviceCall :SetWorkerAcceptLimit, :SetWorkerAcceptLimitResult
81
+ serviceCall :GetWorkerAcceptLimit, :GetWorkerAcceptLimitResult
82
+ serviceCall :BlockWorker, :BlockWorkerResult
83
+ serviceCall :UnblockWorker, :BlockWorkerResult
84
+
85
+ serviceCall :GetFileUploadURL, :GetFileUploadURLResult
86
+ serviceCall :GetAccountBalance, :GetAccountBalanceResult
87
+ serviceCall :GetRequesterStatistic, :GetStatisticResult, { :Count => 1 }
88
+
89
+ serviceCall :NotifyWorkers, :NotifyWorkersResult
90
+
91
+ def initialize(args={})
92
+ newargs = args.dup
93
+ unless args[:Config].nil?
94
+ loaded = Amazon::Util::DataReader.load( args[:Config], :YAML )
95
+ newargs = args.merge loaded.inject({}) {|a,b| a[b[0].to_sym] = b[1] ; a }
96
+ end
97
+ @threadcount = args[:ThreadCount].to_i
98
+ @threadcount = DEFAULT_THREADCOUNT unless @threadcount >= 1
99
+ raise "Cannot override WSDL version ( #{WSDL_VERSION} )" unless args[:Version].nil? or args[:Version].equals? WSDL_VERSION
100
+ super newargs.merge( :Name => :AWSMechanicalTurkRequester,
101
+ :ServiceClass => Amazon::WebServices::MechanicalTurk,
102
+ :Version => WSDL_VERSION )
103
+ end
104
+
105
+ # Create a series of similar HITs, sharing common parameters. Utilizes HITType
106
+ # * hit_template is the array of parameters to pass to createHIT.
107
+ # * question_template will be passed as a template into ERB to generate the :Question parameter
108
+ # * the RequesterAnnotation parameter of hit_template will also be passed through ERB
109
+ # * hit_data_set should consist of an array of hashes defining unique instance variables utilized by question_template
110
+ def createHITs( hit_template, question_template, hit_data_set )
111
+ hit_template = hit_template.dup
112
+ lifetime = hit_template[:LifetimeInSeconds]
113
+ numassignments_template = hit_template[:MaxAssignments]
114
+ annotation_template = hit_template[:RequesterAnnotation]
115
+ hit_template.delete :LifetimeInSeconds
116
+ hit_template.delete :MaxAssignments
117
+ hit_template.delete :RequesterAnnotation
118
+
119
+ ht = hit_template[:HITTypeId] || registerHITType( hit_template )[:HITTypeId]
120
+
121
+ tp = Amazon::Util::ThreadPool.new @threadcount
122
+
123
+ created = [].extend(MonitorMixin)
124
+ failed = [].extend(MonitorMixin)
125
+ hit_data_set.each do |hd|
126
+ tp.addWork(hd) do |hit_data|
127
+ begin
128
+ b = Amazon::Util::Binder.new( hit_data )
129
+ annotation = b.erb_eval( annotation_template )
130
+ numassignments = b.erb_eval( numassignments_template.to_s ).to_i
131
+ question = b.erb_eval( question_template )
132
+ result = self.createHIT( :HITTypeId => ht,
133
+ :LifetimeInSeconds => lifetime,
134
+ :MaxAssignments => ( hit_data[:MaxAssignments] || numassignments || 1 ),
135
+ :Question => question,
136
+ :RequesterAnnotation => ( hit_data[:RequesterAnnotation] || annotation || "")
137
+ )
138
+ created.synchronize do
139
+ created << result
140
+ end
141
+ rescue => e
142
+ failed.synchronize do
143
+ failed << hit_data.merge( :Error => e.message, :Description => e.description )
144
+ end
145
+ end
146
+ end # tp.addWork
147
+ end # hit_data_set.each
148
+ tp.finish
149
+
150
+ return :Created => created, :Failed => failed
151
+ end
152
+
153
+ # Update a series of HITs to belong to a new HITType
154
+ # * hit_template is the array of parameters to pass to registerHITType
155
+ # * hit_ids is a list of HITIds (strings)
156
+ def updateHITs( hit_template, hit_ids )
157
+ hit_template = hit_template.dup
158
+ hit_template.delete :LifetimeInSeconds
159
+ hit_template.delete :RequesterAnnotation
160
+
161
+ hit_type_id = registerHITType( hit_template )[:HITTypeId]
162
+
163
+ tp = Amazon::Util::ThreadPool.new @threadcount
164
+
165
+ updated = [].extend(MonitorMixin)
166
+ failed = [].extend(MonitorMixin)
167
+ hit_ids.each do |hid|
168
+ tp.addWork(hid) do |hit_id|
169
+ begin
170
+ changeHITTypeOfHIT( :HITId => hit_id, :HITTypeId => hit_type_id )
171
+ updated.synchronize do
172
+ updated << hit_id
173
+ end
174
+ rescue => e
175
+ failed.synchronize do
176
+ failed << { :HITId => hit_id, :Error => e.message }
177
+ end
178
+ end
179
+ end # tp.addWork
180
+ end # hit_ids.each
181
+ tp.finish
182
+
183
+ return :Updated => updated, :Failed => failed
184
+ end
185
+
186
+
187
+ # Update a HIT with new properties.
188
+ # hit_id:: Id of the HIT to update
189
+ # hit_template:: hash ( parameter => value ) of parameters to update
190
+ #
191
+ # Acceptable attributes:
192
+ # * Title
193
+ # * Description
194
+ # * Keywords
195
+ # * Reward
196
+ # * QualificationRequirement
197
+ # * AutoApprovalDelayInSeconds
198
+ # * AssignmentDurationInSeconds
199
+ #
200
+ # Behind the scenes, this function retrieves the HIT, merges the HITs
201
+ # current attributes with any you specify, and registers a new HIT
202
+ # Template. It then uses the new ChangeHITTypeOfHIT function to move
203
+ # your HIT to the newly-created HIT Template.
204
+ def updateHIT( hit_id, hit_template )
205
+ hit_template = hit_template.dup
206
+
207
+ hit = getHIT( :HITId => hit_id )
208
+
209
+ props = %w( Title Description Keywords Reward QualificationRequirement
210
+ AutoApprovalDelayInSeconds AssignmentDurationInSeconds
211
+ ).collect {|str| str.to_sym }
212
+
213
+ props.each do |p|
214
+ hit_template[p] = hit[p] if hit_template[p].nil?
215
+ end
216
+
217
+ hit_type_id = registerHITType( hit_template )[:HITTypeId]
218
+
219
+ changeHITTypeOfHIT( :HITId => hit_id, :HITTypeId => hit_type_id )
220
+ end
221
+
222
+
223
+ def getHITResults( list )
224
+ results = [].extend(MonitorMixin)
225
+ tp = Amazon::Util::ThreadPool.new @threadcount
226
+ list.each do |line|
227
+ tp.addWork(line) do |h|
228
+ hit = getHIT( :HITId => h[:HITId] )
229
+ getAssignmentsForHITAll( :HITId => h[:HITId] ).each {|assignment|
230
+ results.synchronize do
231
+ results << ( hit.merge( assignment ) )
232
+ end
233
+ }
234
+ end
235
+ end
236
+ tp.finish
237
+ results.flatten
238
+ end
239
+
240
+ # Returns available funds in USD
241
+ # Calls getAccountBalance and parses out the correct amount
242
+ def availableFunds
243
+ return getAccountBalance[:AvailableBalance][:Amount]
244
+ end
245
+
246
+ # helper function to simplify answer XML
247
+ def simplifyAnswer( answerXML )
248
+ answerHash = Amazon::WebServices::Util::XMLSimplifier.simplify REXML::Document.new(answerXML)
249
+ list = [answerHash[:Answer]].flatten
250
+ list.inject({}) { |list, answer|
251
+ id = answer[:QuestionIdentifier]
252
+ result = answer[:FreeText] || answer[:SelectionIdentifier] || answer[:UploadedFileKey]
253
+ list[id] = result
254
+ list
255
+ }
256
+ end
257
+
258
+ end # MechanicalTurkRequester
259
+
260
+ end # Amazon::WebServices
261
+ end # Amazon
@@ -0,0 +1,136 @@
1
+ # Copyright:: Copyright (c) 2007 Amazon Technologies, Inc.
2
+ # License:: Apache License, Version 2.0
3
+
4
+ require 'amazon/util/logging'
5
+ require 'amazon/webservices/util/validation_exception'
6
+ require 'amazon/webservices/util/unknown_result_exception'
7
+
8
+ module Amazon
9
+ module WebServices
10
+ module MTurk
11
+
12
+ class MechanicalTurkErrorHandler
13
+ include Amazon::Util::Logging
14
+
15
+ REQUIRED_PARAMETERS = [:Relay]
16
+
17
+ # Commands with these prefixes can be retried if we are unsure of success
18
+ RETRY_PRE = %w( search get register update disable assign set dispose )
19
+
20
+ # Max number of times to retry a call
21
+ MAX_RETRY = 6
22
+
23
+ # Base used in Exponential Backoff retry delay
24
+ BACKOFF_BASE = 2
25
+ # Scale factor for Exponential Backoff retry delay
26
+ BACKOFF_INITIAL = 0.1
27
+
28
+ # Matching pattern to find a 'Results' element in the Response
29
+ RESULT_PATTERN = /Result/
30
+ # Additional elements to be considered a 'Result' despite not matching RESULT_PATTERN
31
+ ACCEPTABLE_RESULTS = %w( HIT Qualification QualificationType QualificationRequest Information )
32
+
33
+ def initialize( args )
34
+ missing_parameters = REQUIRED_PARAMETERS - args.keys
35
+ raise "Missing paramters: #{missing_parameters.join(',')}" unless missing_parameters.empty?
36
+ @relay = args[:Relay]
37
+ end
38
+
39
+ def dispatch(method, *args)
40
+ try = 0
41
+ begin
42
+ try += 1
43
+ log "Dispatching call to #{method} (try #{try})"
44
+ response = @relay.send(method,*args)
45
+ validateResponse( response )
46
+ return response
47
+ rescue Exception => error
48
+ case handleError( error,method )
49
+ when :RetryWithBackoff
50
+ retry if doBackoff( try )
51
+ when :RetryImmediate
52
+ retry if canRetry( try )
53
+ when :Ignore
54
+ return :IgnoredError => error
55
+ when :Unknown
56
+ raise Util::UnknownResultException.new( error, method, args )
57
+ when :Fail
58
+ raise error
59
+ else
60
+ raise "Unknown error handling method: #{handleError( error,method )}"
61
+ end
62
+ raise error
63
+ end
64
+ end
65
+
66
+ def methodRetryable( method )
67
+ RETRY_PRE.each do |pre|
68
+ return true if method.to_s =~ /^#{pre}/i
69
+ end
70
+ return false
71
+ end
72
+
73
+ def handleError( error, method )
74
+ log "Handling error: #{error.inspect}"
75
+ case error.class.to_s
76
+ when 'Timeout::Error','SOAP::HTTPStreamError'
77
+ if methodRetryable( method )
78
+ return :RetryImmediate
79
+ else
80
+ return :Unknown
81
+ end
82
+ when 'SOAP::FaultError'
83
+ case error.faultcode.data
84
+ when "aws:Server.ServiceUnavailable"
85
+ return :RetryWithBackoff
86
+ else
87
+ return :Unknown
88
+ end
89
+ when 'Amazon::WebServices::Util::ValidationException'
90
+ return :Fail
91
+ when 'RuntimeError'
92
+ case error.message
93
+ when 'Throttled'
94
+ return :RetryWithBackoff
95
+ else
96
+ return :RetryImmediate
97
+ end
98
+ else
99
+ return :Unknown
100
+ end
101
+ end
102
+
103
+ def canRetry( try )
104
+ try <= MAX_RETRY
105
+ end
106
+
107
+ def doBackoff( try )
108
+ return false unless canRetry(try)
109
+ delay = BACKOFF_INITIAL * ( BACKOFF_BASE ** try )
110
+ sleep delay
111
+ return true
112
+ end
113
+
114
+ def isResultTag( tag )
115
+ tag.to_s =~ RESULT_PATTERN or ACCEPTABLE_RESULTS.include?( tag.to_s )
116
+ end
117
+
118
+ def validateResponse(response)
119
+ log "Validating response: #{response.inspect}"
120
+ raise 'Throttled' if response[:Errors] and response[:Errors][:Error] and response[:Errors][:Error][:Code] == "ServiceUnavailable"
121
+ raise Util::ValidationException.new(response) unless response[:OperationRequest][:Errors].nil?
122
+ resultTags = response.keys.find_all {|r| isResultTag( r ) }
123
+ raise Util::ValidationException.new(response, "Didn't get back an acceptable result tag (got back #{response.keys.join(',')})") if resultTags.empty?
124
+ resultTags.each do |resultTag|
125
+ log "using result tag <#{resultTag}>"
126
+ result = response[resultTag]
127
+ raise Util::ValidationException.new(response) unless result[:Request][:Errors].nil?
128
+ end
129
+ response
130
+ end
131
+
132
+ end # MechanicalTurkErrorHandler
133
+
134
+ end # Amazon::WebServices::MTurk
135
+ end # Amazon::WebServices
136
+ end # Amazon
@@ -0,0 +1,58 @@
1
+ # Copyright:: Copyright (c) 2007 Amazon Technologies, Inc.
2
+ # License:: Apache License, Version 2.0
3
+
4
+ require 'rexml/element'
5
+
6
+ module Amazon
7
+ module WebServices
8
+ module MTurk
9
+
10
+ class QuestionGenerator
11
+
12
+ def self.build(type=:Basic)
13
+ question = self.new(type)
14
+ yield question
15
+ return question.to_xml
16
+ end
17
+
18
+ def initialize(type=:Basic)
19
+ @overview = nil
20
+ @questions = []
21
+ @type = type
22
+ end
23
+
24
+ def ask(*args)
25
+ case @type
26
+ when :Basic
27
+ askBasic( args.join )
28
+ end
29
+ end
30
+
31
+ def askBasic(text)
32
+ id = "BasicQuestion#{@questions.size+1}"
33
+ question = REXML::Element.new 'Text'
34
+ question.text = text
35
+ answerSpec = "<FreeTextAnswer/>"
36
+ @questions << { :Id => id, :Question => question.to_s, :AnswerSpec => answerSpec }
37
+ end
38
+
39
+ def to_xml
40
+ components = [PREAMBLE]
41
+ components << OVERVIEW % @overview unless @overview.nil?
42
+ for question in @questions
43
+ components << QUESTION % [ question[:Id], question[:Question], question[:AnswerSpec] ]
44
+ end
45
+ components << [TAIL]
46
+ return components.join
47
+ end
48
+
49
+ PREAMBLE = '<?xml version="1.0" encoding="UTF-8"?>'+"\n"+'<QuestionForm xmlns="http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchemas/2005-10-01/QuestionForm.xsd">'
50
+ OVERVIEW = '<Overview>%s</Overview>'
51
+ QUESTION = '<Question><QuestionIdentifier>%s</QuestionIdentifier><QuestionContent>%s</QuestionContent><AnswerSpecification>%s</AnswerSpecification></Question>'
52
+ TAIL = '</QuestionForm>'
53
+
54
+ end # QuestionGenerator
55
+
56
+ end # Amazon::WebServices::MTurk
57
+ end # Amazon::WebServices
58
+ end # Amazon