lms.backend.canvas.common
1import datetime 2import http 3import re 4import typing 5 6import edq.net.request 7import edq.util.json 8import edq.util.time 9import requests 10 11DEFAULT_PAGE_SIZE: int = 95 12HEADER_LINK: str = 'Link' 13 14def fetch_next_canvas_link(response: requests.Response) -> typing.Union[str, None]: 15 """ 16 Fetch the Canvas-style next link within the headers. 17 If there is no next link, return None. 18 """ 19 20 headers = response.headers 21 22 if (HEADER_LINK not in headers): 23 return None 24 25 links = headers[HEADER_LINK].split(',') 26 for link in links: 27 parts = link.split(';') 28 if (len(parts) != 2): 29 continue 30 31 if (parts[1].strip() != 'rel="next"'): 32 continue 33 34 return str(parts[0].strip().strip('<>')) 35 36 return None 37 38def make_request( 39 method: str, 40 url: str, 41 raise_on_404: bool = False, 42 json: bool = True, 43 **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 44 """ Make a single Canvas request and return the decoded JSON body. """ 45 46 try: 47 _, body_text = edq.net.request.make_request(method, url, **kwargs) 48 except requests.HTTPError as ex: 49 if (raise_on_404 or (ex.response is None) or (ex.response.status_code != http.HTTPStatus.NOT_FOUND)): 50 raise ex 51 52 return None 53 54 if (not json): 55 return body_text 56 57 return edq.util.json.loads(body_text, strict = True) 58 59def make_get_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 60 """ Make a single Canvas GET request. """ 61 62 return make_request('GET', url, **kwargs) 63 64def make_post_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 65 """ Make a single Canvas POST request. """ 66 67 return make_request('POST', url, **kwargs) 68 69def make_delete_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 70 """ Make a single Canvas DELETE request. """ 71 72 return make_request('DELETE', url, **kwargs) 73 74def make_get_request_list( 75 url: str, 76 headers: typing.Dict[str, typing.Any], 77 data: typing.Union[typing.Dict[str, typing.Any], None] = None, 78 raise_on_404: bool = False, 79 ) -> typing.Union[typing.List[typing.Dict[str, typing.Any]], None]: 80 """ Repeatedly call make_get_request() (using a JSON body and next link) until there are no more results. """ 81 82 output: typing.List[typing.Dict[str, typing.Any]] = [] 83 84 next_url: typing.Union[str, None] = url 85 86 while (next_url is not None): 87 try: 88 response, body_text = edq.net.request.make_get(next_url, headers = headers, data = data) 89 except requests.HTTPError as ex: 90 if (raise_on_404 or (ex.response is None) or (ex.response.status_code != http.HTTPStatus.NOT_FOUND)): 91 raise ex 92 93 return None 94 95 next_url = fetch_next_canvas_link(response) 96 97 new_results = edq.util.json.loads(body_text, strict = True) 98 for new_result in new_results: 99 output.append(new_result) 100 101 return output 102 103def parse_timestamp(value: typing.Union[str, None]) -> typing.Union[edq.util.time.Timestamp, None]: 104 """ Parse a Canvas-style timestamp into a common form. """ 105 106 if (value is None): 107 return None 108 109 # Parse out some cases that Python <= 3.10 cannot deal with. 110 value = re.sub(r'Z$', '+00:00', value) 111 value = re.sub(r'(\d\d:\d\d)(\.\d+)', r'\1', value) 112 113 pytime = datetime.datetime.fromisoformat(value) 114 return edq.util.time.Timestamp.from_pytime(pytime)
DEFAULT_PAGE_SIZE: int =
95
HEADER_LINK: str =
'Link'
def
fetch_next_canvas_link(response: requests.models.Response) -> Optional[str]:
15def fetch_next_canvas_link(response: requests.Response) -> typing.Union[str, None]: 16 """ 17 Fetch the Canvas-style next link within the headers. 18 If there is no next link, return None. 19 """ 20 21 headers = response.headers 22 23 if (HEADER_LINK not in headers): 24 return None 25 26 links = headers[HEADER_LINK].split(',') 27 for link in links: 28 parts = link.split(';') 29 if (len(parts) != 2): 30 continue 31 32 if (parts[1].strip() != 'rel="next"'): 33 continue 34 35 return str(parts[0].strip().strip('<>')) 36 37 return None
Fetch the Canvas-style next link within the headers. If there is no next link, return None.
def
make_request( method: str, url: str, raise_on_404: bool = False, json: bool = True, **kwargs: Any) -> Optional[Any]:
39def make_request( 40 method: str, 41 url: str, 42 raise_on_404: bool = False, 43 json: bool = True, 44 **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 45 """ Make a single Canvas request and return the decoded JSON body. """ 46 47 try: 48 _, body_text = edq.net.request.make_request(method, url, **kwargs) 49 except requests.HTTPError as ex: 50 if (raise_on_404 or (ex.response is None) or (ex.response.status_code != http.HTTPStatus.NOT_FOUND)): 51 raise ex 52 53 return None 54 55 if (not json): 56 return body_text 57 58 return edq.util.json.loads(body_text, strict = True)
Make a single Canvas request and return the decoded JSON body.
def
make_get_request(url: str, **kwargs: Any) -> Optional[Any]:
60def make_get_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 61 """ Make a single Canvas GET request. """ 62 63 return make_request('GET', url, **kwargs)
Make a single Canvas GET request.
def
make_post_request(url: str, **kwargs: Any) -> Optional[Any]:
65def make_post_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 66 """ Make a single Canvas POST request. """ 67 68 return make_request('POST', url, **kwargs)
Make a single Canvas POST request.
def
make_delete_request(url: str, **kwargs: Any) -> Optional[Any]:
70def make_delete_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 71 """ Make a single Canvas DELETE request. """ 72 73 return make_request('DELETE', url, **kwargs)
Make a single Canvas DELETE request.
def
make_get_request_list( url: str, headers: Dict[str, Any], data: Optional[Dict[str, Any]] = None, raise_on_404: bool = False) -> Optional[List[Dict[str, Any]]]:
75def make_get_request_list( 76 url: str, 77 headers: typing.Dict[str, typing.Any], 78 data: typing.Union[typing.Dict[str, typing.Any], None] = None, 79 raise_on_404: bool = False, 80 ) -> typing.Union[typing.List[typing.Dict[str, typing.Any]], None]: 81 """ Repeatedly call make_get_request() (using a JSON body and next link) until there are no more results. """ 82 83 output: typing.List[typing.Dict[str, typing.Any]] = [] 84 85 next_url: typing.Union[str, None] = url 86 87 while (next_url is not None): 88 try: 89 response, body_text = edq.net.request.make_get(next_url, headers = headers, data = data) 90 except requests.HTTPError as ex: 91 if (raise_on_404 or (ex.response is None) or (ex.response.status_code != http.HTTPStatus.NOT_FOUND)): 92 raise ex 93 94 return None 95 96 next_url = fetch_next_canvas_link(response) 97 98 new_results = edq.util.json.loads(body_text, strict = True) 99 for new_result in new_results: 100 output.append(new_result) 101 102 return output
Repeatedly call make_get_request() (using a JSON body and next link) until there are no more results.
def
parse_timestamp(value: Optional[str]) -> Optional[edq.util.time.Timestamp]:
104def parse_timestamp(value: typing.Union[str, None]) -> typing.Union[edq.util.time.Timestamp, None]: 105 """ Parse a Canvas-style timestamp into a common form. """ 106 107 if (value is None): 108 return None 109 110 # Parse out some cases that Python <= 3.10 cannot deal with. 111 value = re.sub(r'Z$', '+00:00', value) 112 value = re.sub(r'(\d\d:\d\d)(\.\d+)', r'\1', value) 113 114 pytime = datetime.datetime.fromisoformat(value) 115 return edq.util.time.Timestamp.from_pytime(pytime)
Parse a Canvas-style timestamp into a common form.