Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2)

Side by Side Diff: third_party/gsutil/boto/mturk/connection.py

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

Powered by Google App Engine
This is Rietveld 408576698