comparison env/lib/python3.9/site-packages/boto/mturk/connection.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
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)