Mercurial > repos > shellac > guppy_basecaller
comparison env/lib/python3.7/site-packages/boto/mturk/connection.py @ 0:26e78fe6e8c4 draft
"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
author | shellac |
---|---|
date | Sat, 02 May 2020 07:14:21 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:26e78fe6e8c4 |
---|---|
1 # Copyright (c) 2006,2007 Mitch Garnaat http://garnaat.org/ | |
2 # | |
3 # Permission is hereby granted, free of charge, to any person obtaining a | |
4 # copy of this software and associated documentation files (the | |
5 # "Software"), to deal in the Software without restriction, including | |
6 # without limitation the rights to use, copy, modify, merge, publish, dis- | |
7 # tribute, sublicense, and/or sell copies of the Software, and to permit | |
8 # persons to whom the Software is furnished to do so, subject to the fol- | |
9 # lowing conditions: | |
10 # | |
11 # The above copyright notice and this permission notice shall be included | |
12 # in all copies or substantial portions of the Software. | |
13 # | |
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- | |
16 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT | |
17 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
18 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
19 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS | |
20 # IN THE SOFTWARE. | |
21 import xml.sax | |
22 import datetime | |
23 import itertools | |
24 | |
25 from boto import handler | |
26 from boto import config | |
27 from boto.mturk.price import Price | |
28 import boto.mturk.notification | |
29 from boto.connection import AWSQueryConnection | |
30 from boto.exception import EC2ResponseError | |
31 from boto.resultset import ResultSet | |
32 from boto.mturk.question import QuestionForm, ExternalQuestion, HTMLQuestion | |
33 | |
34 | |
35 class MTurkRequestError(EC2ResponseError): | |
36 "Error for MTurk Requests" | |
37 # todo: subclass from an abstract parent of EC2ResponseError | |
38 | |
39 | |
40 class MTurkConnection(AWSQueryConnection): | |
41 | |
42 APIVersion = '2014-08-15' | |
43 | |
44 def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, | |
45 is_secure=True, port=None, proxy=None, proxy_port=None, | |
46 proxy_user=None, proxy_pass=None, | |
47 host=None, debug=0, | |
48 https_connection_factory=None, security_token=None, | |
49 profile_name=None): | |
50 if not host: | |
51 if config.has_option('MTurk', 'sandbox') and config.get('MTurk', 'sandbox') == 'True': | |
52 host = 'mechanicalturk.sandbox.amazonaws.com' | |
53 else: | |
54 host = 'mechanicalturk.amazonaws.com' | |
55 self.debug = debug | |
56 | |
57 super(MTurkConnection, self).__init__(aws_access_key_id, | |
58 aws_secret_access_key, | |
59 is_secure, port, proxy, proxy_port, | |
60 proxy_user, proxy_pass, host, debug, | |
61 https_connection_factory, | |
62 security_token=security_token, | |
63 profile_name=profile_name) | |
64 | |
65 def _required_auth_capability(self): | |
66 return ['mturk'] | |
67 | |
68 def get_account_balance(self): | |
69 """ | |
70 """ | |
71 params = {} | |
72 return self._process_request('GetAccountBalance', params, | |
73 [('AvailableBalance', Price), | |
74 ('OnHoldBalance', Price)]) | |
75 | |
76 def register_hit_type(self, title, description, reward, duration, | |
77 keywords=None, approval_delay=None, qual_req=None): | |
78 """ | |
79 Register a new HIT Type | |
80 title, description are strings | |
81 reward is a Price object | |
82 duration can be a timedelta, or an object castable to an int | |
83 """ | |
84 params = dict( | |
85 Title=title, | |
86 Description=description, | |
87 AssignmentDurationInSeconds=self.duration_as_seconds(duration), | |
88 ) | |
89 params.update(MTurkConnection.get_price_as_price(reward).get_as_params('Reward')) | |
90 | |
91 if keywords: | |
92 params['Keywords'] = self.get_keywords_as_string(keywords) | |
93 | |
94 if approval_delay is not None: | |
95 d = self.duration_as_seconds(approval_delay) | |
96 params['AutoApprovalDelayInSeconds'] = d | |
97 | |
98 if qual_req is not None: | |
99 params.update(qual_req.get_as_params()) | |
100 | |
101 return self._process_request('RegisterHITType', params, | |
102 [('HITTypeId', HITTypeId)]) | |
103 | |
104 def set_email_notification(self, hit_type, email, event_types=None): | |
105 """ | |
106 Performs a SetHITTypeNotification operation to set email | |
107 notification for a specified HIT type | |
108 """ | |
109 return self._set_notification(hit_type, 'Email', email, | |
110 'SetHITTypeNotification', event_types) | |
111 | |
112 def set_rest_notification(self, hit_type, url, event_types=None): | |
113 """ | |
114 Performs a SetHITTypeNotification operation to set REST notification | |
115 for a specified HIT type | |
116 """ | |
117 return self._set_notification(hit_type, 'REST', url, | |
118 'SetHITTypeNotification', event_types) | |
119 | |
120 def set_sqs_notification(self, hit_type, queue_url, event_types=None): | |
121 """ | |
122 Performs a SetHITTypeNotification operation so set SQS notification | |
123 for a specified HIT type. Queue URL is of form: | |
124 https://queue.amazonaws.com/<CUSTOMER_ID>/<QUEUE_NAME> and can be | |
125 found when looking at the details for a Queue in the AWS Console | |
126 """ | |
127 return self._set_notification(hit_type, "SQS", queue_url, | |
128 'SetHITTypeNotification', event_types) | |
129 | |
130 def send_test_event_notification(self, hit_type, url, | |
131 event_types=None, | |
132 test_event_type='Ping'): | |
133 """ | |
134 Performs a SendTestEventNotification operation with REST notification | |
135 for a specified HIT type | |
136 """ | |
137 return self._set_notification(hit_type, 'REST', url, | |
138 'SendTestEventNotification', | |
139 event_types, test_event_type) | |
140 | |
141 def _set_notification(self, hit_type, transport, | |
142 destination, request_type, | |
143 event_types=None, test_event_type=None): | |
144 """ | |
145 Common operation to set notification or send a test event | |
146 notification for a specified HIT type | |
147 """ | |
148 params = {'HITTypeId': hit_type} | |
149 | |
150 # from the Developer Guide: | |
151 # The 'Active' parameter is optional. If omitted, the active status of | |
152 # the HIT type's notification specification is unchanged. All HIT types | |
153 # begin with their notification specifications in the "inactive" status. | |
154 notification_params = {'Destination': destination, | |
155 'Transport': transport, | |
156 'Version': boto.mturk.notification.NotificationMessage.NOTIFICATION_VERSION, | |
157 'Active': True, | |
158 } | |
159 | |
160 # add specific event types if required | |
161 if event_types: | |
162 self.build_list_params(notification_params, event_types, | |
163 'EventType') | |
164 | |
165 # Set up dict of 'Notification.1.Transport' etc. values | |
166 notification_rest_params = {} | |
167 num = 1 | |
168 for key in notification_params: | |
169 notification_rest_params['Notification.%d.%s' % (num, key)] = notification_params[key] | |
170 | |
171 # Update main params dict | |
172 params.update(notification_rest_params) | |
173 | |
174 # If test notification, specify the notification type to be tested | |
175 if test_event_type: | |
176 params.update({'TestEventType': test_event_type}) | |
177 | |
178 # Execute operation | |
179 return self._process_request(request_type, params) | |
180 | |
181 def create_hit(self, hit_type=None, question=None, hit_layout=None, | |
182 lifetime=datetime.timedelta(days=7), | |
183 max_assignments=1, | |
184 title=None, description=None, keywords=None, | |
185 reward=None, duration=datetime.timedelta(days=7), | |
186 approval_delay=None, annotation=None, | |
187 questions=None, qualifications=None, | |
188 layout_params=None, response_groups=None): | |
189 """ | |
190 Creates a new HIT. | |
191 Returns a ResultSet | |
192 See: http://docs.amazonwebservices.com/AWSMechTurk/2012-03-25/AWSMturkAPI/ApiReference_CreateHITOperation.html | |
193 """ | |
194 | |
195 # Handle basic required arguments and set up params dict | |
196 params = {'LifetimeInSeconds': | |
197 self.duration_as_seconds(lifetime), | |
198 'MaxAssignments': max_assignments, | |
199 } | |
200 | |
201 # handle single or multiple questions or layouts | |
202 neither = question is None and questions is None | |
203 if hit_layout is None: | |
204 both = question is not None and questions is not None | |
205 if neither or both: | |
206 raise ValueError("Must specify question (single Question instance) or questions (list or QuestionForm instance), but not both") | |
207 if question: | |
208 questions = [question] | |
209 question_param = QuestionForm(questions) | |
210 if isinstance(question, QuestionForm): | |
211 question_param = question | |
212 elif isinstance(question, ExternalQuestion): | |
213 question_param = question | |
214 elif isinstance(question, HTMLQuestion): | |
215 question_param = question | |
216 params['Question'] = question_param.get_as_xml() | |
217 else: | |
218 if not neither: | |
219 raise ValueError("Must not specify question (single Question instance) or questions (list or QuestionForm instance) when specifying hit_layout") | |
220 params['HITLayoutId'] = hit_layout | |
221 if layout_params: | |
222 params.update(layout_params.get_as_params()) | |
223 | |
224 # if hit type specified then add it | |
225 # else add the additional required parameters | |
226 if hit_type: | |
227 params['HITTypeId'] = hit_type | |
228 else: | |
229 # Handle keywords | |
230 final_keywords = MTurkConnection.get_keywords_as_string(keywords) | |
231 | |
232 # Handle price argument | |
233 final_price = MTurkConnection.get_price_as_price(reward) | |
234 | |
235 final_duration = self.duration_as_seconds(duration) | |
236 | |
237 additional_params = dict( | |
238 Title=title, | |
239 Description=description, | |
240 Keywords=final_keywords, | |
241 AssignmentDurationInSeconds=final_duration, | |
242 ) | |
243 additional_params.update(final_price.get_as_params('Reward')) | |
244 | |
245 if approval_delay is not None: | |
246 d = self.duration_as_seconds(approval_delay) | |
247 additional_params['AutoApprovalDelayInSeconds'] = d | |
248 | |
249 # add these params to the others | |
250 params.update(additional_params) | |
251 | |
252 # add the annotation if specified | |
253 if annotation is not None: | |
254 params['RequesterAnnotation'] = annotation | |
255 | |
256 # Add the Qualifications if specified | |
257 if qualifications is not None: | |
258 params.update(qualifications.get_as_params()) | |
259 | |
260 # Handle optional response groups argument | |
261 if response_groups: | |
262 self.build_list_params(params, response_groups, 'ResponseGroup') | |
263 | |
264 # Submit | |
265 return self._process_request('CreateHIT', params, [('HIT', HIT)]) | |
266 | |
267 def change_hit_type_of_hit(self, hit_id, hit_type): | |
268 """ | |
269 Change the HIT type of an existing HIT. Note that the reward associated | |
270 with the new HIT type must match the reward of the current HIT type in | |
271 order for the operation to be valid. | |
272 | |
273 :type hit_id: str | |
274 :type hit_type: str | |
275 """ | |
276 params = {'HITId': hit_id, | |
277 'HITTypeId': hit_type} | |
278 | |
279 return self._process_request('ChangeHITTypeOfHIT', params) | |
280 | |
281 def get_reviewable_hits(self, hit_type=None, status='Reviewable', | |
282 sort_by='Expiration', sort_direction='Ascending', | |
283 page_size=10, page_number=1): | |
284 """ | |
285 Retrieve the HITs that have a status of Reviewable, or HITs that | |
286 have a status of Reviewing, and that belong to the Requester | |
287 calling the operation. | |
288 """ | |
289 params = {'Status': status, | |
290 'SortProperty': sort_by, | |
291 'SortDirection': sort_direction, | |
292 'PageSize': page_size, | |
293 'PageNumber': page_number} | |
294 | |
295 # Handle optional hit_type argument | |
296 if hit_type is not None: | |
297 params.update({'HITTypeId': hit_type}) | |
298 | |
299 return self._process_request('GetReviewableHITs', params, | |
300 [('HIT', HIT)]) | |
301 | |
302 @staticmethod | |
303 def _get_pages(page_size, total_records): | |
304 """ | |
305 Given a page size (records per page) and a total number of | |
306 records, return the page numbers to be retrieved. | |
307 """ | |
308 pages = total_records / page_size + bool(total_records % page_size) | |
309 return list(range(1, pages + 1)) | |
310 | |
311 def get_all_hits(self): | |
312 """ | |
313 Return all of a Requester's HITs | |
314 | |
315 Despite what search_hits says, it does not return all hits, but | |
316 instead returns a page of hits. This method will pull the hits | |
317 from the server 100 at a time, but will yield the results | |
318 iteratively, so subsequent requests are made on demand. | |
319 """ | |
320 page_size = 100 | |
321 search_rs = self.search_hits(page_size=page_size) | |
322 total_records = int(search_rs.TotalNumResults) | |
323 get_page_hits = lambda page: self.search_hits(page_size=page_size, page_number=page) | |
324 page_nums = self._get_pages(page_size, total_records) | |
325 hit_sets = itertools.imap(get_page_hits, page_nums) | |
326 return itertools.chain.from_iterable(hit_sets) | |
327 | |
328 def search_hits(self, sort_by='CreationTime', sort_direction='Ascending', | |
329 page_size=10, page_number=1, response_groups=None): | |
330 """ | |
331 Return a page of a Requester's HITs, on behalf of the Requester. | |
332 The operation returns HITs of any status, except for HITs that | |
333 have been disposed with the DisposeHIT operation. | |
334 Note: | |
335 The SearchHITs operation does not accept any search parameters | |
336 that filter the results. | |
337 """ | |
338 params = {'SortProperty': sort_by, | |
339 'SortDirection': sort_direction, | |
340 'PageSize': page_size, | |
341 'PageNumber': page_number} | |
342 # Handle optional response groups argument | |
343 if response_groups: | |
344 self.build_list_params(params, response_groups, 'ResponseGroup') | |
345 | |
346 return self._process_request('SearchHITs', params, [('HIT', HIT)]) | |
347 | |
348 def get_assignment(self, assignment_id, response_groups=None): | |
349 """ | |
350 Retrieves an assignment using the assignment's ID. Requesters can only | |
351 retrieve their own assignments, and only assignments whose related HIT | |
352 has not been disposed. | |
353 | |
354 The returned ResultSet will have the following attributes: | |
355 | |
356 Request | |
357 This element is present only if the Request ResponseGroup | |
358 is specified. | |
359 Assignment | |
360 The assignment. The response includes one Assignment object. | |
361 HIT | |
362 The HIT associated with this assignment. The response | |
363 includes one HIT object. | |
364 | |
365 """ | |
366 | |
367 params = {'AssignmentId': assignment_id} | |
368 | |
369 # Handle optional response groups argument | |
370 if response_groups: | |
371 self.build_list_params(params, response_groups, 'ResponseGroup') | |
372 | |
373 return self._process_request('GetAssignment', params, | |
374 [('Assignment', Assignment), | |
375 ('HIT', HIT)]) | |
376 | |
377 def get_assignments(self, hit_id, status=None, | |
378 sort_by='SubmitTime', sort_direction='Ascending', | |
379 page_size=10, page_number=1, response_groups=None): | |
380 """ | |
381 Retrieves completed assignments for a HIT. | |
382 Use this operation to retrieve the results for a HIT. | |
383 | |
384 The returned ResultSet will have the following attributes: | |
385 | |
386 NumResults | |
387 The number of assignments on the page in the filtered results | |
388 list, equivalent to the number of assignments being returned | |
389 by this call. | |
390 A non-negative integer, as a string. | |
391 PageNumber | |
392 The number of the page in the filtered results list being | |
393 returned. | |
394 A positive integer, as a string. | |
395 TotalNumResults | |
396 The total number of HITs in the filtered results list based | |
397 on this call. | |
398 A non-negative integer, as a string. | |
399 | |
400 The ResultSet will contain zero or more Assignment objects | |
401 | |
402 """ | |
403 params = {'HITId': hit_id, | |
404 'SortProperty': sort_by, | |
405 'SortDirection': sort_direction, | |
406 'PageSize': page_size, | |
407 'PageNumber': page_number} | |
408 | |
409 if status is not None: | |
410 params['AssignmentStatus'] = status | |
411 | |
412 # Handle optional response groups argument | |
413 if response_groups: | |
414 self.build_list_params(params, response_groups, 'ResponseGroup') | |
415 | |
416 return self._process_request('GetAssignmentsForHIT', params, | |
417 [('Assignment', Assignment)]) | |
418 | |
419 def approve_assignment(self, assignment_id, feedback=None): | |
420 """ | |
421 """ | |
422 params = {'AssignmentId': assignment_id} | |
423 if feedback: | |
424 params['RequesterFeedback'] = feedback | |
425 return self._process_request('ApproveAssignment', params) | |
426 | |
427 def reject_assignment(self, assignment_id, feedback=None): | |
428 """ | |
429 """ | |
430 params = {'AssignmentId': assignment_id} | |
431 if feedback: | |
432 params['RequesterFeedback'] = feedback | |
433 return self._process_request('RejectAssignment', params) | |
434 | |
435 def approve_rejected_assignment(self, assignment_id, feedback=None): | |
436 """ | |
437 """ | |
438 params = {'AssignmentId': assignment_id} | |
439 if feedback: | |
440 params['RequesterFeedback'] = feedback | |
441 return self._process_request('ApproveRejectedAssignment', params) | |
442 | |
443 def get_file_upload_url(self, assignment_id, question_identifier): | |
444 """ | |
445 Generates and returns a temporary URL to an uploaded file. The | |
446 temporary URL is used to retrieve the file as an answer to a | |
447 FileUploadAnswer question, it is valid for 60 seconds. | |
448 | |
449 Will have a FileUploadURL attribute as per the API Reference. | |
450 """ | |
451 | |
452 params = {'AssignmentId': assignment_id, | |
453 'QuestionIdentifier': question_identifier} | |
454 | |
455 return self._process_request('GetFileUploadURL', params, | |
456 [('FileUploadURL', FileUploadURL)]) | |
457 | |
458 def get_hit(self, hit_id, response_groups=None): | |
459 """ | |
460 """ | |
461 params = {'HITId': hit_id} | |
462 # Handle optional response groups argument | |
463 if response_groups: | |
464 self.build_list_params(params, response_groups, 'ResponseGroup') | |
465 | |
466 return self._process_request('GetHIT', params, [('HIT', HIT)]) | |
467 | |
468 def set_reviewing(self, hit_id, revert=None): | |
469 """ | |
470 Update a HIT with a status of Reviewable to have a status of Reviewing, | |
471 or reverts a Reviewing HIT back to the Reviewable status. | |
472 | |
473 Only HITs with a status of Reviewable can be updated with a status of | |
474 Reviewing. Similarly, only Reviewing HITs can be reverted back to a | |
475 status of Reviewable. | |
476 """ | |
477 params = {'HITId': hit_id} | |
478 if revert: | |
479 params['Revert'] = revert | |
480 return self._process_request('SetHITAsReviewing', params) | |
481 | |
482 def disable_hit(self, hit_id, response_groups=None): | |
483 """ | |
484 Remove a HIT from the Mechanical Turk marketplace, approves all | |
485 submitted assignments that have not already been approved or rejected, | |
486 and disposes of the HIT and all assignment data. | |
487 | |
488 Assignments for the HIT that have already been submitted, but not yet | |
489 approved or rejected, will be automatically approved. Assignments in | |
490 progress at the time of the call to DisableHIT will be approved once | |
491 the assignments are submitted. You will be charged for approval of | |
492 these assignments. DisableHIT completely disposes of the HIT and | |
493 all submitted assignment data. Assignment results data cannot be | |
494 retrieved for a HIT that has been disposed. | |
495 | |
496 It is not possible to re-enable a HIT once it has been disabled. | |
497 To make the work from a disabled HIT available again, create a new HIT. | |
498 """ | |
499 params = {'HITId': hit_id} | |
500 # Handle optional response groups argument | |
501 if response_groups: | |
502 self.build_list_params(params, response_groups, 'ResponseGroup') | |
503 | |
504 return self._process_request('DisableHIT', params) | |
505 | |
506 def dispose_hit(self, hit_id): | |
507 """ | |
508 Dispose of a HIT that is no longer needed. | |
509 | |
510 Only HITs in the "reviewable" state, with all submitted | |
511 assignments approved or rejected, can be disposed. A Requester | |
512 can call GetReviewableHITs to determine which HITs are | |
513 reviewable, then call GetAssignmentsForHIT to retrieve the | |
514 assignments. Disposing of a HIT removes the HIT from the | |
515 results of a call to GetReviewableHITs. """ | |
516 params = {'HITId': hit_id} | |
517 return self._process_request('DisposeHIT', params) | |
518 | |
519 def expire_hit(self, hit_id): | |
520 | |
521 """ | |
522 Expire a HIT that is no longer needed. | |
523 | |
524 The effect is identical to the HIT expiring on its own. The | |
525 HIT no longer appears on the Mechanical Turk web site, and no | |
526 new Workers are allowed to accept the HIT. Workers who have | |
527 accepted the HIT prior to expiration are allowed to complete | |
528 it or return it, or allow the assignment duration to elapse | |
529 (abandon the HIT). Once all remaining assignments have been | |
530 submitted, the expired HIT becomes"reviewable", and will be | |
531 returned by a call to GetReviewableHITs. | |
532 """ | |
533 params = {'HITId': hit_id} | |
534 return self._process_request('ForceExpireHIT', params) | |
535 | |
536 def extend_hit(self, hit_id, assignments_increment=None, | |
537 expiration_increment=None): | |
538 """ | |
539 Increase the maximum number of assignments, or extend the | |
540 expiration date, of an existing HIT. | |
541 | |
542 NOTE: If a HIT has a status of Reviewable and the HIT is | |
543 extended to make it Available, the HIT will not be returned by | |
544 GetReviewableHITs, and its submitted assignments will not be | |
545 returned by GetAssignmentsForHIT, until the HIT is Reviewable | |
546 again. Assignment auto-approval will still happen on its | |
547 original schedule, even if the HIT has been extended. Be sure | |
548 to retrieve and approve (or reject) submitted assignments | |
549 before extending the HIT, if so desired. | |
550 """ | |
551 # must provide assignment *or* expiration increment | |
552 if (assignments_increment is None and expiration_increment is None) or \ | |
553 (assignments_increment is not None and expiration_increment is not None): | |
554 raise ValueError("Must specify either assignments_increment or expiration_increment, but not both") | |
555 | |
556 params = {'HITId': hit_id} | |
557 if assignments_increment: | |
558 params['MaxAssignmentsIncrement'] = assignments_increment | |
559 if expiration_increment: | |
560 params['ExpirationIncrementInSeconds'] = expiration_increment | |
561 | |
562 return self._process_request('ExtendHIT', params) | |
563 | |
564 def get_help(self, about, help_type='Operation'): | |
565 """ | |
566 Return information about the Mechanical Turk Service | |
567 operations and response group NOTE - this is basically useless | |
568 as it just returns the URL of the documentation | |
569 | |
570 help_type: either 'Operation' or 'ResponseGroup' | |
571 """ | |
572 params = {'About': about, 'HelpType': help_type} | |
573 return self._process_request('Help', params) | |
574 | |
575 def grant_bonus(self, worker_id, assignment_id, bonus_price, reason): | |
576 """ | |
577 Issues a payment of money from your account to a Worker. To | |
578 be eligible for a bonus, the Worker must have submitted | |
579 results for one of your HITs, and have had those results | |
580 approved or rejected. This payment happens separately from the | |
581 reward you pay to the Worker when you approve the Worker's | |
582 assignment. The Bonus must be passed in as an instance of the | |
583 Price object. | |
584 """ | |
585 params = bonus_price.get_as_params('BonusAmount', 1) | |
586 params['WorkerId'] = worker_id | |
587 params['AssignmentId'] = assignment_id | |
588 params['Reason'] = reason | |
589 | |
590 return self._process_request('GrantBonus', params) | |
591 | |
592 def block_worker(self, worker_id, reason): | |
593 """ | |
594 Block a worker from working on my tasks. | |
595 """ | |
596 params = {'WorkerId': worker_id, 'Reason': reason} | |
597 | |
598 return self._process_request('BlockWorker', params) | |
599 | |
600 def unblock_worker(self, worker_id, reason): | |
601 """ | |
602 Unblock a worker from working on my tasks. | |
603 """ | |
604 params = {'WorkerId': worker_id, 'Reason': reason} | |
605 | |
606 return self._process_request('UnblockWorker', params) | |
607 | |
608 def notify_workers(self, worker_ids, subject, message_text): | |
609 """ | |
610 Send a text message to workers. | |
611 """ | |
612 params = {'Subject': subject, | |
613 'MessageText': message_text} | |
614 self.build_list_params(params, worker_ids, 'WorkerId') | |
615 | |
616 return self._process_request('NotifyWorkers', params) | |
617 | |
618 def create_qualification_type(self, | |
619 name, | |
620 description, | |
621 status, | |
622 keywords=None, | |
623 retry_delay=None, | |
624 test=None, | |
625 answer_key=None, | |
626 answer_key_xml=None, | |
627 test_duration=None, | |
628 auto_granted=False, | |
629 auto_granted_value=1): | |
630 """ | |
631 Create a new Qualification Type. | |
632 | |
633 name: This will be visible to workers and must be unique for a | |
634 given requester. | |
635 | |
636 description: description shown to workers. Max 2000 characters. | |
637 | |
638 status: 'Active' or 'Inactive' | |
639 | |
640 keywords: list of keyword strings or comma separated string. | |
641 Max length of 1000 characters when concatenated with commas. | |
642 | |
643 retry_delay: number of seconds after requesting a | |
644 qualification the worker must wait before they can ask again. | |
645 If not specified, workers can only request this qualification | |
646 once. | |
647 | |
648 test: a QuestionForm | |
649 | |
650 answer_key: an XML string of your answer key, for automatically | |
651 scored qualification tests. | |
652 (Consider implementing an AnswerKey class for this to support.) | |
653 | |
654 test_duration: the number of seconds a worker has to complete the test. | |
655 | |
656 auto_granted: if True, requests for the Qualification are granted | |
657 immediately. Can't coexist with a test. | |
658 | |
659 auto_granted_value: auto_granted qualifications are given this value. | |
660 | |
661 """ | |
662 | |
663 params = {'Name': name, | |
664 'Description': description, | |
665 'QualificationTypeStatus': status, | |
666 } | |
667 if retry_delay is not None: | |
668 params['RetryDelayInSeconds'] = retry_delay | |
669 | |
670 if test is not None: | |
671 assert(isinstance(test, QuestionForm)) | |
672 assert(test_duration is not None) | |
673 params['Test'] = test.get_as_xml() | |
674 | |
675 if test_duration is not None: | |
676 params['TestDurationInSeconds'] = test_duration | |
677 | |
678 if answer_key is not None: | |
679 if isinstance(answer_key, basestring): | |
680 params['AnswerKey'] = answer_key # xml | |
681 else: | |
682 raise TypeError | |
683 # Eventually someone will write an AnswerKey class. | |
684 | |
685 if auto_granted: | |
686 assert(test is None) | |
687 params['AutoGranted'] = True | |
688 params['AutoGrantedValue'] = auto_granted_value | |
689 | |
690 if keywords: | |
691 params['Keywords'] = self.get_keywords_as_string(keywords) | |
692 | |
693 return self._process_request('CreateQualificationType', params, | |
694 [('QualificationType', | |
695 QualificationType)]) | |
696 | |
697 def get_qualification_type(self, qualification_type_id): | |
698 params = {'QualificationTypeId': qualification_type_id } | |
699 return self._process_request('GetQualificationType', params, | |
700 [('QualificationType', QualificationType)]) | |
701 | |
702 def get_all_qualifications_for_qual_type(self, qualification_type_id): | |
703 page_size = 100 | |
704 search_qual = self.get_qualifications_for_qualification_type(qualification_type_id) | |
705 total_records = int(search_qual.TotalNumResults) | |
706 get_page_quals = lambda page: self.get_qualifications_for_qualification_type(qualification_type_id = qualification_type_id, page_size=page_size, page_number = page) | |
707 page_nums = self._get_pages(page_size, total_records) | |
708 qual_sets = itertools.imap(get_page_quals, page_nums) | |
709 return itertools.chain.from_iterable(qual_sets) | |
710 | |
711 def get_qualifications_for_qualification_type(self, qualification_type_id, page_size=100, page_number = 1): | |
712 params = {'QualificationTypeId': qualification_type_id, | |
713 'PageSize': page_size, | |
714 'PageNumber': page_number} | |
715 return self._process_request('GetQualificationsForQualificationType', params, | |
716 [('Qualification', Qualification)]) | |
717 | |
718 def update_qualification_type(self, qualification_type_id, | |
719 description=None, | |
720 status=None, | |
721 retry_delay=None, | |
722 test=None, | |
723 answer_key=None, | |
724 test_duration=None, | |
725 auto_granted=None, | |
726 auto_granted_value=None): | |
727 | |
728 params = {'QualificationTypeId': qualification_type_id} | |
729 | |
730 if description is not None: | |
731 params['Description'] = description | |
732 | |
733 if status is not None: | |
734 params['QualificationTypeStatus'] = status | |
735 | |
736 if retry_delay is not None: | |
737 params['RetryDelayInSeconds'] = retry_delay | |
738 | |
739 if test is not None: | |
740 assert(isinstance(test, QuestionForm)) | |
741 params['Test'] = test.get_as_xml() | |
742 | |
743 if test_duration is not None: | |
744 params['TestDurationInSeconds'] = test_duration | |
745 | |
746 if answer_key is not None: | |
747 if isinstance(answer_key, basestring): | |
748 params['AnswerKey'] = answer_key # xml | |
749 else: | |
750 raise TypeError | |
751 # Eventually someone will write an AnswerKey class. | |
752 | |
753 if auto_granted is not None: | |
754 params['AutoGranted'] = auto_granted | |
755 | |
756 if auto_granted_value is not None: | |
757 params['AutoGrantedValue'] = auto_granted_value | |
758 | |
759 return self._process_request('UpdateQualificationType', params, | |
760 [('QualificationType', QualificationType)]) | |
761 | |
762 def dispose_qualification_type(self, qualification_type_id): | |
763 """TODO: Document.""" | |
764 params = {'QualificationTypeId': qualification_type_id} | |
765 return self._process_request('DisposeQualificationType', params) | |
766 | |
767 def search_qualification_types(self, query=None, sort_by='Name', | |
768 sort_direction='Ascending', page_size=10, | |
769 page_number=1, must_be_requestable=True, | |
770 must_be_owned_by_caller=True): | |
771 """TODO: Document.""" | |
772 params = {'Query': query, | |
773 'SortProperty': sort_by, | |
774 'SortDirection': sort_direction, | |
775 'PageSize': page_size, | |
776 'PageNumber': page_number, | |
777 'MustBeRequestable': must_be_requestable, | |
778 'MustBeOwnedByCaller': must_be_owned_by_caller} | |
779 return self._process_request('SearchQualificationTypes', params, | |
780 [('QualificationType', QualificationType)]) | |
781 | |
782 def get_qualification_requests(self, qualification_type_id, | |
783 sort_by='Expiration', | |
784 sort_direction='Ascending', page_size=10, | |
785 page_number=1): | |
786 """TODO: Document.""" | |
787 params = {'QualificationTypeId': qualification_type_id, | |
788 'SortProperty': sort_by, | |
789 'SortDirection': sort_direction, | |
790 'PageSize': page_size, | |
791 'PageNumber': page_number} | |
792 return self._process_request('GetQualificationRequests', params, | |
793 [('QualificationRequest', QualificationRequest)]) | |
794 | |
795 def grant_qualification(self, qualification_request_id, integer_value=1): | |
796 """TODO: Document.""" | |
797 params = {'QualificationRequestId': qualification_request_id, | |
798 'IntegerValue': integer_value} | |
799 return self._process_request('GrantQualification', params) | |
800 | |
801 def revoke_qualification(self, subject_id, qualification_type_id, | |
802 reason=None): | |
803 """TODO: Document.""" | |
804 params = {'SubjectId': subject_id, | |
805 'QualificationTypeId': qualification_type_id, | |
806 'Reason': reason} | |
807 return self._process_request('RevokeQualification', params) | |
808 | |
809 def assign_qualification(self, qualification_type_id, worker_id, | |
810 value=1, send_notification=True): | |
811 params = {'QualificationTypeId': qualification_type_id, | |
812 'WorkerId' : worker_id, | |
813 'IntegerValue' : value, | |
814 'SendNotification' : send_notification} | |
815 return self._process_request('AssignQualification', params) | |
816 | |
817 def get_qualification_score(self, qualification_type_id, worker_id): | |
818 """TODO: Document.""" | |
819 params = {'QualificationTypeId' : qualification_type_id, | |
820 'SubjectId' : worker_id} | |
821 return self._process_request('GetQualificationScore', params, | |
822 [('Qualification', Qualification)]) | |
823 | |
824 def update_qualification_score(self, qualification_type_id, worker_id, | |
825 value): | |
826 """TODO: Document.""" | |
827 params = {'QualificationTypeId' : qualification_type_id, | |
828 'SubjectId' : worker_id, | |
829 'IntegerValue' : value} | |
830 return self._process_request('UpdateQualificationScore', params) | |
831 | |
832 def _process_request(self, request_type, params, marker_elems=None): | |
833 """ | |
834 Helper to process the xml response from AWS | |
835 """ | |
836 params['Operation'] = request_type | |
837 response = self.make_request(None, params, verb='POST') | |
838 return self._process_response(response, marker_elems) | |
839 | |
840 def _process_response(self, response, marker_elems=None): | |
841 """ | |
842 Helper to process the xml response from AWS | |
843 """ | |
844 body = response.read() | |
845 if self.debug == 2: | |
846 print(body) | |
847 if '<Errors>' not in body.decode('utf-8'): | |
848 rs = ResultSet(marker_elems) | |
849 h = handler.XmlHandler(rs, self) | |
850 xml.sax.parseString(body, h) | |
851 return rs | |
852 else: | |
853 raise MTurkRequestError(response.status, response.reason, body) | |
854 | |
855 @staticmethod | |
856 def get_keywords_as_string(keywords): | |
857 """ | |
858 Returns a comma+space-separated string of keywords from either | |
859 a list or a string | |
860 """ | |
861 if isinstance(keywords, list): | |
862 keywords = ', '.join(keywords) | |
863 if isinstance(keywords, str): | |
864 final_keywords = keywords | |
865 elif isinstance(keywords, unicode): | |
866 final_keywords = keywords.encode('utf-8') | |
867 elif keywords is None: | |
868 final_keywords = "" | |
869 else: | |
870 raise TypeError("keywords argument must be a string or a list of strings; got a %s" % type(keywords)) | |
871 return final_keywords | |
872 | |
873 @staticmethod | |
874 def get_price_as_price(reward): | |
875 """ | |
876 Returns a Price data structure from either a float or a Price | |
877 """ | |
878 if isinstance(reward, Price): | |
879 final_price = reward | |
880 else: | |
881 final_price = Price(reward) | |
882 return final_price | |
883 | |
884 @staticmethod | |
885 def duration_as_seconds(duration): | |
886 if isinstance(duration, datetime.timedelta): | |
887 duration = duration.days * 86400 + duration.seconds | |
888 try: | |
889 duration = int(duration) | |
890 except TypeError: | |
891 raise TypeError("Duration must be a timedelta or int-castable, got %s" % type(duration)) | |
892 return duration | |
893 | |
894 | |
895 class BaseAutoResultElement(object): | |
896 """ | |
897 Base class to automatically add attributes when parsing XML | |
898 """ | |
899 def __init__(self, connection): | |
900 pass | |
901 | |
902 def startElement(self, name, attrs, connection): | |
903 return None | |
904 | |
905 def endElement(self, name, value, connection): | |
906 setattr(self, name, value) | |
907 | |
908 | |
909 class HIT(BaseAutoResultElement): | |
910 """ | |
911 Class to extract a HIT structure from a response (used in ResultSet) | |
912 | |
913 Will have attributes named as per the Developer Guide, | |
914 e.g. HITId, HITTypeId, CreationTime | |
915 """ | |
916 | |
917 # property helper to determine if HIT has expired | |
918 def _has_expired(self): | |
919 """ Has this HIT expired yet? """ | |
920 expired = False | |
921 if hasattr(self, 'Expiration'): | |
922 now = datetime.datetime.utcnow() | |
923 expiration = datetime.datetime.strptime(self.Expiration, '%Y-%m-%dT%H:%M:%SZ') | |
924 expired = (now >= expiration) | |
925 else: | |
926 raise ValueError("ERROR: Request for expired property, but no Expiration in HIT!") | |
927 return expired | |
928 | |
929 # are we there yet? | |
930 expired = property(_has_expired) | |
931 | |
932 | |
933 class FileUploadURL(BaseAutoResultElement): | |
934 """ | |
935 Class to extract an FileUploadURL structure from a response | |
936 """ | |
937 | |
938 pass | |
939 | |
940 | |
941 class HITTypeId(BaseAutoResultElement): | |
942 """ | |
943 Class to extract an HITTypeId structure from a response | |
944 """ | |
945 | |
946 pass | |
947 | |
948 | |
949 class Qualification(BaseAutoResultElement): | |
950 """ | |
951 Class to extract an Qualification structure from a response (used in | |
952 ResultSet) | |
953 | |
954 Will have attributes named as per the Developer Guide such as | |
955 QualificationTypeId, IntegerValue. Does not seem to contain GrantTime. | |
956 """ | |
957 | |
958 pass | |
959 | |
960 | |
961 class QualificationType(BaseAutoResultElement): | |
962 """ | |
963 Class to extract an QualificationType structure from a response (used in | |
964 ResultSet) | |
965 | |
966 Will have attributes named as per the Developer Guide, | |
967 e.g. QualificationTypeId, CreationTime, Name, etc | |
968 """ | |
969 | |
970 pass | |
971 | |
972 | |
973 class QualificationRequest(BaseAutoResultElement): | |
974 """ | |
975 Class to extract an QualificationRequest structure from a response (used in | |
976 ResultSet) | |
977 | |
978 Will have attributes named as per the Developer Guide, | |
979 e.g. QualificationRequestId, QualificationTypeId, SubjectId, etc | |
980 """ | |
981 | |
982 def __init__(self, connection): | |
983 super(QualificationRequest, self).__init__(connection) | |
984 self.answers = [] | |
985 | |
986 def endElement(self, name, value, connection): | |
987 # the answer consists of embedded XML, so it needs to be parsed independantly | |
988 if name == 'Answer': | |
989 answer_rs = ResultSet([('Answer', QuestionFormAnswer)]) | |
990 h = handler.XmlHandler(answer_rs, connection) | |
991 value = connection.get_utf8_value(value) | |
992 xml.sax.parseString(value, h) | |
993 self.answers.append(answer_rs) | |
994 else: | |
995 super(QualificationRequest, self).endElement(name, value, connection) | |
996 | |
997 | |
998 class Assignment(BaseAutoResultElement): | |
999 """ | |
1000 Class to extract an Assignment structure from a response (used in | |
1001 ResultSet) | |
1002 | |
1003 Will have attributes named as per the Developer Guide, | |
1004 e.g. AssignmentId, WorkerId, HITId, Answer, etc | |
1005 """ | |
1006 | |
1007 def __init__(self, connection): | |
1008 super(Assignment, self).__init__(connection) | |
1009 self.answers = [] | |
1010 | |
1011 def endElement(self, name, value, connection): | |
1012 # the answer consists of embedded XML, so it needs to be parsed independantly | |
1013 if name == 'Answer': | |
1014 answer_rs = ResultSet([('Answer', QuestionFormAnswer)]) | |
1015 h = handler.XmlHandler(answer_rs, connection) | |
1016 value = connection.get_utf8_value(value) | |
1017 xml.sax.parseString(value, h) | |
1018 self.answers.append(answer_rs) | |
1019 else: | |
1020 super(Assignment, self).endElement(name, value, connection) | |
1021 | |
1022 | |
1023 class QuestionFormAnswer(BaseAutoResultElement): | |
1024 """ | |
1025 Class to extract Answers from inside the embedded XML | |
1026 QuestionFormAnswers element inside the Answer element which is | |
1027 part of the Assignment and QualificationRequest structures | |
1028 | |
1029 A QuestionFormAnswers element contains an Answer element for each | |
1030 question in the HIT or Qualification test for which the Worker | |
1031 provided an answer. Each Answer contains a QuestionIdentifier | |
1032 element whose value corresponds to the QuestionIdentifier of a | |
1033 Question in the QuestionForm. See the QuestionForm data structure | |
1034 for more information about questions and answer specifications. | |
1035 | |
1036 If the question expects a free-text answer, the Answer element | |
1037 contains a FreeText element. This element contains the Worker's | |
1038 answer | |
1039 | |
1040 *NOTE* - currently really only supports free-text and selection answers | |
1041 """ | |
1042 | |
1043 def __init__(self, connection): | |
1044 super(QuestionFormAnswer, self).__init__(connection) | |
1045 self.fields = [] | |
1046 self.qid = None | |
1047 | |
1048 def endElement(self, name, value, connection): | |
1049 if name == 'QuestionIdentifier': | |
1050 self.qid = value | |
1051 elif name in ['FreeText', 'SelectionIdentifier', 'OtherSelectionText'] and self.qid: | |
1052 self.fields.append(value) |