├── .gitignore ├── README.md ├── grading-assigner.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | Requires the `requests` module, which you can either install globally or in a virtualenv. 3 | 4 | ``` 5 | usage: grading-assigner.py [-h] [--auth-token TOKEN] [--debug] 6 | [--certification] 7 | [--ids IDS_QUEUED [IDS_QUEUED ...]] 8 | 9 | Poll the Udacity reviews API to claim projects to review. 10 | 11 | optional arguments: 12 | -h, --help show this help message and exit 13 | --auth-token TOKEN, -T TOKEN 14 | Your Udacity auth token. To obtain, login to 15 | review.udacity.com, open the Javascript console, and 16 | copy the output of 17 | `JSON.parse(localStorage.currentUser).token`. This can 18 | also be stored in the environment variable 19 | UDACITY_AUTH_TOKEN. 20 | --debug, -d Turn on debug statements. 21 | --certification, -c Retrieve current certifications. 22 | --ids IDS_QUEUED [IDS_QUEUED ...], -ids IDS_QUEUED [IDS_QUEUED ...] 23 | projects ids to queue separated by spaces, i.e.: -ids 28 38 139 24 | 25 | ``` 26 | 27 | # Example 28 | ``` 29 | python grading_assigment.py -ids 107 104 147 30 | |2016-10-18 10:23:04,097| Requesting certifications... 31 | |2016-10-18 10:23:05,334| Found certifications for project IDs: [104, 106, 108, 147, 234, 105, 236, 235, 107, 28, 27, 103] in languages [u'es', u'en'] 32 | |2016-10-18 10:23:05,334| Polling for new submissions... 33 | 34 | 35 | Selected projects to queue: 36 | 37 | name | id | price | status | hashtag 38 | Building a Student Intervention System | 104 | 45.0 | certified | nanodegree,machinelearning 39 | Titanic Survival Exploration | 147 | 7.0 | certified | nanodegree,machinelearning 40 | Investigate a Dataset | 107 | 50.0 | certified | nanodegree,dataanalysis 41 | 42 | 43 | 44 | 45 | |2016-10-18 10:23:05,335| Will poll for projects/languages [{'project_id': 104, 'language': u'es'}, {'project_id': 104, 'language': u'en'}, {'project_id': 147, 'language': u'es'}, {'project_id': 147, 'language': u'en'}, {'project_id': 107, 'language': u'es'}, {'project_id': 107, 'language': u'en'}] 46 | |2016-10-18 10:23:06,348| Waiting for assigned submissions < 2 47 | ``` 48 | 49 | Press ctrl-c to quit. 50 | -------------------------------------------------------------------------------- /grading-assigner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import signal 3 | import sys 4 | import argparse 5 | import logging 6 | import os 7 | import requests 8 | import time 9 | import pytz 10 | from dateutil import parser 11 | from datetime import datetime, timedelta 12 | 13 | utc = pytz.UTC 14 | 15 | # Script config 16 | BASE_URL = 'https://review-api.udacity.com/api/v1' 17 | CERTS_URL = '{}/me/certifications.json'.format(BASE_URL) 18 | ME_URL = '{}/me'.format(BASE_URL) 19 | ME_REQUEST_URL = '{}/me/submission_requests.json'.format(BASE_URL) 20 | CREATE_REQUEST_URL = '{}/submission_requests.json'.format(BASE_URL) 21 | DELETE_URL_TMPL = '{}/submission_requests/{}.json' 22 | GET_REQUEST_URL_TMPL = '{}/submission_requests/{}.json' 23 | PUT_REQUEST_URL_TMPL = '{}/submission_requests/{}.json' 24 | REFRESH_URL_TMPL = '{}/submission_requests/{}/refresh.json' 25 | ASSIGNED_COUNT_URL = '{}/me/submissions/assigned_count.json'.format(BASE_URL) 26 | ASSIGNED_URL = '{}/me/submissions/assigned.json'.format(BASE_URL) 27 | 28 | REVIEW_URL = 'https://review.udacity.com/#!/submissions/{sid}' 29 | REQUESTS_PER_SECOND = 1 # Please leave this alone. 30 | 31 | logging.basicConfig(format='|%(asctime)s| %(message)s') 32 | logger = logging.getLogger(__name__) 33 | logger.setLevel(logging.INFO) 34 | 35 | headers = None 36 | 37 | def signal_handler(signal, frame): 38 | if headers: 39 | logger.info('Cleaning up active request') 40 | me_resp = requests.get(ME_REQUEST_URL, headers=headers) 41 | if me_resp.status_code == 200 and len(me_resp.json()) > 0: 42 | logger.info(DELETE_URL_TMPL.format(BASE_URL, me_resp.json()[0]['id'])) 43 | del_resp = requests.delete(DELETE_URL_TMPL.format(BASE_URL, me_resp.json()[0]['id']), 44 | headers=headers) 45 | logger.info(del_resp) 46 | sys.exit(0) 47 | 48 | signal.signal(signal.SIGINT, signal_handler) 49 | 50 | def alert_for_assignment(current_request, headers): 51 | if current_request and current_request['status'] == 'fulfilled': 52 | logger.info("") 53 | logger.info("=================================================") 54 | logger.info("You have been assigned to grade a new submission!") 55 | logger.info("View it here: " + REVIEW_URL.format(sid=current_request['submission_id'])) 56 | logger.info("=================================================") 57 | logger.info("Continuing to poll...") 58 | return None 59 | return current_request 60 | 61 | def wait_for_assign_eligible(): 62 | while True: 63 | assigned_resp = requests.get(ASSIGNED_COUNT_URL, headers=headers) 64 | if assigned_resp.status_code == 404 or assigned_resp.json()['assigned_count'] < 2: 65 | break 66 | else: 67 | logger.info('Waiting for assigned submissions < 2') 68 | # Wait 30 seconds before checking to see if < 2 open submissions 69 | # that is, waiting until a create submission request will be permitted 70 | time.sleep(30.0) 71 | 72 | def refresh_request(current_request): 73 | logger.info('Refreshing existing request') 74 | refresh_resp = requests.put(REFRESH_URL_TMPL.format(BASE_URL, current_request['id']), 75 | headers=headers) 76 | refresh_resp.raise_for_status() 77 | if refresh_resp.status_code == 404: 78 | logger.info('No active request was found/refreshed. Loop and either wait for < 2 to be assigned or immediately create') 79 | return None 80 | else: 81 | return refresh_resp.json() 82 | 83 | def fetch_certified_pairs(): 84 | logger.info("Requesting certifications...") 85 | me_resp = requests.get(ME_URL, headers=headers) 86 | me_resp.raise_for_status() 87 | languages = me_resp.json()['application']['languages'] or ['en-us'] 88 | 89 | certs_resp = requests.get(CERTS_URL, headers=headers) 90 | certs_resp.raise_for_status() 91 | 92 | certs = certs_resp.json() 93 | project_ids = [cert['project']['id'] for cert in certs if cert['status'] == 'certified'] 94 | 95 | logger.info("Found certifications for project IDs: %s in languages %s", 96 | str(project_ids), str(languages)) 97 | logger.info("Polling for new submissions...") 98 | 99 | return [{'project_id': project_id, 'language': lang} for project_id in project_ids for lang in languages] 100 | 101 | def request_reviews(token): 102 | global headers 103 | headers = {'Authorization': token, 'Content-Length': '0'} 104 | 105 | project_language_pairs = fetch_certified_pairs() 106 | logger.info("Will poll for projects/languages %s", str(project_language_pairs)) 107 | 108 | me_req_resp = requests.get(ME_REQUEST_URL, headers=headers) 109 | current_request = me_req_resp.json()[0] if me_req_resp.status_code == 201 and len(me_req_resp.json()) > 0 else None 110 | if current_request: 111 | update_resp = requests.put(PUT_REQUEST_URL_TMPL.format(BASE_URL, current_request['id']), 112 | json={'projects': project_language_pairs}, headers=headers) 113 | current_request = update_resp.json() if update_resp.status_code == 200 else current_request 114 | 115 | while True: 116 | # Loop and wait until fewer than 2 reviews assigned, as creating 117 | # a request will fail 118 | wait_for_assign_eligible() 119 | 120 | if current_request is None: 121 | logger.info('Creating a request for ' + str(len(project_language_pairs)) + 122 | ' possible project/language combinations') 123 | create_resp = requests.post(CREATE_REQUEST_URL, 124 | json={'projects': project_language_pairs}, 125 | headers=headers) 126 | current_request = create_resp.json() if create_resp.status_code == 201 else None 127 | else: 128 | logger.info(current_request) 129 | closing_at = parser.parse(current_request['closed_at']) 130 | 131 | utcnow = datetime.utcnow() 132 | utcnow = utcnow.replace(tzinfo=pytz.utc) 133 | 134 | if closing_at < utcnow + timedelta(minutes=59): 135 | # Refreshing a request is more costly than just loading 136 | # and only needs to be done to ensure the request doesn't 137 | # expire (1 hour) 138 | logger.info('0-0-0-0-0-0-0-0-0-0- refreshing request 0-0-0-0-0-0-0') 139 | current_request = refresh_request(current_request) 140 | else: 141 | logger.info('Checking for new assignments') 142 | # If an assignment has been made since status was last checked, 143 | # the request record will no longer be 'fulfilled' 144 | url = GET_REQUEST_URL_TMPL.format(BASE_URL, current_request['id']) 145 | get_req_resp = requests.get(url, headers=headers) 146 | current_request = get_req_resp.json() if me_req_resp.status_code == 200 else None 147 | 148 | current_request = alert_for_assignment(current_request, headers) 149 | if current_request: 150 | # Wait 2 minutes before next check to see if the request has been fulfilled 151 | time.sleep(120.0) 152 | 153 | if __name__ == "__main__": 154 | cmd_parser = argparse.ArgumentParser(description = 155 | "Poll the Udacity reviews API to claim projects to review." 156 | ) 157 | cmd_parser.add_argument('--auth-token', '-T', dest='token', 158 | metavar='TOKEN', type=str, 159 | action='store', default=os.environ.get('UDACITY_AUTH_TOKEN'), 160 | help=""" 161 | Your Udacity auth token. To obtain, login to review.udacity.com, open the Javascript console, and copy the output of `JSON.parse(localStorage.currentUser).token`. This can also be stored in the environment variable UDACITY_AUTH_TOKEN. 162 | """ 163 | ) 164 | cmd_parser.add_argument('--debug', '-d', action='store_true', help='Turn on debug statements.') 165 | args = cmd_parser.parse_args() 166 | 167 | if not args.token: 168 | cmd_parser.print_help() 169 | cmd_parser.exit() 170 | 171 | if args.debug: 172 | logger.setLevel(logging.DEBUG) 173 | 174 | request_reviews(args.token) 175 | 176 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.9.1 2 | --------------------------------------------------------------------------------