autograder.util.net
Utilities for network and HTTP.
1""" 2Utilities for network and HTTP. 3""" 4 5import typing 6 7import edq.util.json 8import requests 9 10import autograder.api.constants 11import autograder.testing.asserts 12import autograder.testing.constants 13import autograder.testing.model 14 15CLEAN_REMOVE_HEADERS: typing.Set[str] = { 16 'access-control-allow-origin', 17 'transfer-encoding', 18} 19""" Headers to remove from API responses. """ 20 21GRADING_ENDPOINTS: typing.Set[str] = { 22 'courses/assignments/submissions/submit', 23 'courses/assignments/submissions/proxy/regrade', 24 'courses/assignments/submissions/proxy/resubmit', 25 'courses/assignments/submissions/proxy/submit', 26} 27""" 28These endpoints need to have additional timestamps normalized. 29Note that the endpoint constants are copied to avoid cyclic dependencies. 30""" 31 32FULL_NORMALIZE_TIMESTAMP_KEYS: typing.Set[str] = autograder.testing.asserts.NORMALIZE_TIMESTAMP_KEYS | { 33 'grading_end_time', 34 'grading_start_time', 35 'proxy_end_time', 36 'proxy_start_time', 37 'regrade-cutoff', 38} 39""" Keys for timestamp values to normalize. """ 40 41def clean_api_response(response: requests.Response, body: str) -> str: 42 """ 43 Clean autograder API responses (so they can be stored consistently). 44 """ 45 46 # Clean response headers. 47 for header in CLEAN_REMOVE_HEADERS: 48 response.headers.pop(header, None) 49 50 # Most responses are JSON. 51 try: 52 data = edq.util.json.loads(body, strict = True) 53 except Exception: 54 # Response is not JSON. 55 return body 56 57 # Standardize top-level (metadata) keys. 58 data['start-timestamp'] = 0 59 data['end-timestamp'] = 0 60 data['id'] = '00000000-0000-0000-0000-000000000000' 61 62 # Standardize content keys. 63 64 # Clean specific timestamps. 65 66 endpoint = response.url.strip().split(f"api/{autograder.api.constants.API_VERSION}/")[-1] 67 68 timestamp_keys = autograder.testing.asserts.NORMALIZE_TIMESTAMP_KEYS 69 if (endpoint in GRADING_ENDPOINTS): 70 timestamp_keys = FULL_NORMALIZE_TIMESTAMP_KEYS 71 72 data = autograder.testing.asserts._normalize_timestamps(data, keys = timestamp_keys) 73 74 # Endpoint-Specific Tasks 75 76 if (endpoint in GRADING_ENDPOINTS): 77 # Normalize submission IDs. 78 content = data.get('content', None) 79 if (content is not None): 80 result = content.get('result', None) 81 if (result is not None): 82 parts = result['id'].split('::') 83 parts[3] = str(autograder.testing.constants.TEST_TIMESTAMP) 84 result['id'] = '::'.join(parts) 85 86 result['short-id'] = str(autograder.testing.constants.TEST_TIMESTAMP) 87 88 # A test case returns a stack trace, normalize it. 89 if ("ModuleNotFoundError: No module named 'ZZZ'" in result.get('epilogue', '')): 90 result['epilogue'] = autograder.testing.constants.TEST_CRASH_EPILOGUE 91 elif (endpoint == 'system/stacks'): 92 # Replace payloads for stack traces completely to make it consistent. 93 data['content'] = autograder.testing.model.STACK_TRACE_PAYLOAD 94 elif (endpoint == 'courses/assignments/images/fetch'): 95 # Write a dummy docker image (which is just a text file). 96 content = data.get('content', None) 97 if (content is not None): 98 content['bytes'] = autograder.testing.constants.TEST_PAYLOAD_B64_GZIP_BYTES 99 content['image-info']['created-timestamp'] = autograder.testing.constants.TEST_TIMESTAMP 100 content['image-info']['gzip-size-bytes'] = len(autograder.testing.constants.TEST_PAYLOAD_GZIP_BYTES) 101 content['image-info']['size-bytes'] = len(autograder.testing.constants.TEST_PAYLOAD_BYTES) 102 elif (endpoint == 'courses/assignments/images/info'): 103 content = data.get('content', None) 104 if (content is not None): 105 content['image-info']['created-timestamp'] = autograder.testing.constants.TEST_TIMESTAMP 106 content['image-info']['size-bytes'] = len(autograder.testing.constants.TEST_PAYLOAD_BYTES) 107 elif ('tokens' in endpoint): 108 # Normalize Tokens 109 content = data.get('content', None) 110 if (content is not None): 111 autograder.testing.asserts._noramlize_tokens(content) 112 113 # Convert body back to a string. 114 body = edq.util.json.dumps(data) 115 116 return body
CLEAN_REMOVE_HEADERS: Set[str] =
{'access-control-allow-origin', 'transfer-encoding'}
Headers to remove from API responses.
GRADING_ENDPOINTS: Set[str] =
{'courses/assignments/submissions/proxy/regrade', 'courses/assignments/submissions/proxy/submit', 'courses/assignments/submissions/proxy/resubmit', 'courses/assignments/submissions/submit'}
These endpoints need to have additional timestamps normalized. Note that the endpoint constants are copied to avoid cyclic dependencies.
FULL_NORMALIZE_TIMESTAMP_KEYS: Set[str] =
{'grading_end_time', 'regrade-cutoff', 'analysis-timestamp', 'proxy_start_time', 'grading_start_time', 'proxy_end_time', 'first-timestamp', 'last-timestamp'}
Keys for timestamp values to normalize.
def
clean_api_response(response: requests.models.Response, body: str) -> str:
42def clean_api_response(response: requests.Response, body: str) -> str: 43 """ 44 Clean autograder API responses (so they can be stored consistently). 45 """ 46 47 # Clean response headers. 48 for header in CLEAN_REMOVE_HEADERS: 49 response.headers.pop(header, None) 50 51 # Most responses are JSON. 52 try: 53 data = edq.util.json.loads(body, strict = True) 54 except Exception: 55 # Response is not JSON. 56 return body 57 58 # Standardize top-level (metadata) keys. 59 data['start-timestamp'] = 0 60 data['end-timestamp'] = 0 61 data['id'] = '00000000-0000-0000-0000-000000000000' 62 63 # Standardize content keys. 64 65 # Clean specific timestamps. 66 67 endpoint = response.url.strip().split(f"api/{autograder.api.constants.API_VERSION}/")[-1] 68 69 timestamp_keys = autograder.testing.asserts.NORMALIZE_TIMESTAMP_KEYS 70 if (endpoint in GRADING_ENDPOINTS): 71 timestamp_keys = FULL_NORMALIZE_TIMESTAMP_KEYS 72 73 data = autograder.testing.asserts._normalize_timestamps(data, keys = timestamp_keys) 74 75 # Endpoint-Specific Tasks 76 77 if (endpoint in GRADING_ENDPOINTS): 78 # Normalize submission IDs. 79 content = data.get('content', None) 80 if (content is not None): 81 result = content.get('result', None) 82 if (result is not None): 83 parts = result['id'].split('::') 84 parts[3] = str(autograder.testing.constants.TEST_TIMESTAMP) 85 result['id'] = '::'.join(parts) 86 87 result['short-id'] = str(autograder.testing.constants.TEST_TIMESTAMP) 88 89 # A test case returns a stack trace, normalize it. 90 if ("ModuleNotFoundError: No module named 'ZZZ'" in result.get('epilogue', '')): 91 result['epilogue'] = autograder.testing.constants.TEST_CRASH_EPILOGUE 92 elif (endpoint == 'system/stacks'): 93 # Replace payloads for stack traces completely to make it consistent. 94 data['content'] = autograder.testing.model.STACK_TRACE_PAYLOAD 95 elif (endpoint == 'courses/assignments/images/fetch'): 96 # Write a dummy docker image (which is just a text file). 97 content = data.get('content', None) 98 if (content is not None): 99 content['bytes'] = autograder.testing.constants.TEST_PAYLOAD_B64_GZIP_BYTES 100 content['image-info']['created-timestamp'] = autograder.testing.constants.TEST_TIMESTAMP 101 content['image-info']['gzip-size-bytes'] = len(autograder.testing.constants.TEST_PAYLOAD_GZIP_BYTES) 102 content['image-info']['size-bytes'] = len(autograder.testing.constants.TEST_PAYLOAD_BYTES) 103 elif (endpoint == 'courses/assignments/images/info'): 104 content = data.get('content', None) 105 if (content is not None): 106 content['image-info']['created-timestamp'] = autograder.testing.constants.TEST_TIMESTAMP 107 content['image-info']['size-bytes'] = len(autograder.testing.constants.TEST_PAYLOAD_BYTES) 108 elif ('tokens' in endpoint): 109 # Normalize Tokens 110 content = data.get('content', None) 111 if (content is not None): 112 autograder.testing.asserts._noramlize_tokens(content) 113 114 # Convert body back to a string. 115 body = edq.util.json.dumps(data) 116 117 return body
Clean autograder API responses (so they can be stored consistently).