lms.backend.canvas.common
1import datetime 2import http 3import re 4import typing 5 6import html2text 7 8import edq.net.request 9import edq.util.json 10import edq.util.time 11import requests 12 13DEFAULT_PAGE_SIZE: int = 95 14HEADER_LINK: str = 'Link' 15 16def fetch_next_canvas_link(response: requests.Response) -> typing.Union[str, None]: 17 """ 18 Fetch the Canvas-style next link within the headers. 19 If there is no next link, return None. 20 """ 21 22 headers = response.headers 23 24 if (HEADER_LINK not in headers): 25 return None 26 27 links = headers[HEADER_LINK].split(',') 28 for link in links: 29 parts = link.split(';') 30 if (len(parts) != 2): 31 continue 32 33 if (parts[1].strip() != 'rel="next"'): 34 continue 35 36 return str(parts[0].strip().strip('<>')) 37 38 return None 39 40def make_request( 41 method: str, 42 url: str, 43 raise_on_404: bool = False, 44 json: bool = True, 45 **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 46 """ Make a single Canvas request and return the decoded JSON body. """ 47 48 try: 49 _, body_text = edq.net.request.make_request(method, url, **kwargs) 50 except requests.HTTPError as ex: 51 if (raise_on_404 or (ex.response is None) or (ex.response.status_code != http.HTTPStatus.NOT_FOUND)): 52 raise ex 53 54 return None 55 56 if (not json): 57 return body_text 58 59 return edq.util.json.loads(body_text, strict = True) 60 61def make_get_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 62 """ Make a single Canvas GET request. """ 63 64 return make_request('GET', url, **kwargs) 65 66def make_post_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 67 """ Make a single Canvas POST request. """ 68 69 return make_request('POST', url, **kwargs) 70 71def make_put_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 72 """ Make a single Canvas PUT request. """ 73 74 return make_request('PUT', url, **kwargs) 75 76def make_delete_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 77 """ Make a single Canvas DELETE request. """ 78 79 return make_request('DELETE', url, **kwargs) 80 81def make_get_request_list( 82 url: str, 83 headers: typing.Dict[str, typing.Any], 84 data: typing.Union[typing.Dict[str, typing.Any], None] = None, 85 raise_on_404: bool = False, 86 ) -> typing.Union[typing.List[typing.Dict[str, typing.Any]], None]: 87 """ Repeatedly call make_get_request() (using a JSON body and next link) until there are no more results. """ 88 89 output: typing.List[typing.Dict[str, typing.Any]] = [] 90 91 next_url: typing.Union[str, None] = url 92 93 while (next_url is not None): 94 try: 95 response, body_text = edq.net.request.make_get(next_url, headers = headers, data = data) 96 except requests.HTTPError as ex: 97 if (raise_on_404 or (ex.response is None) or (ex.response.status_code != http.HTTPStatus.NOT_FOUND)): 98 raise ex 99 100 return None 101 102 next_url = fetch_next_canvas_link(response) 103 104 new_results = edq.util.json.loads(body_text, strict = True) 105 for new_result in new_results: 106 output.append(new_result) 107 108 return output 109 110def parse_timestamp(value: typing.Union[str, None]) -> typing.Union[edq.util.time.Timestamp, None]: 111 """ Parse a Canvas-style timestamp into a common form. """ 112 113 if (value is None): 114 return None 115 116 # Parse out some cases that Python <= 3.10 cannot deal with. 117 value = re.sub(r'Z$', '+00:00', value) 118 value = re.sub(r'(\d\d:\d\d)(\.\d+)', r'\1', value) 119 120 pytime = datetime.datetime.fromisoformat(value) 121 return edq.util.time.Timestamp.from_pytime(pytime) 122 123def html_to_markdown(html: typing.Union[str, None]) -> str: 124 """ 125 Parse the text from a Canvas quiz question into markdown. 126 We intend for the resulting markdown to have a little HTML as possible. 127 This is an impossible task, but we want to do our best. 128 """ 129 130 if (html is None): 131 return '' 132 133 converter = html2text.HTML2Text() 134 135 converter.body_width = 0 136 converter.mark_code = True 137 138 text = converter.handle(html) 139 text = text.strip() 140 141 # Replace code tags with fences. 142 text = re.sub(r'\[/?code\]', '```', text) 143 144 # Replace placeholders (e.g., for fill in the blank questions). 145 text = re.sub(r'\[(\w+?)\]', r'<placeholder>\1</placeholder>', text) 146 147 return text
DEFAULT_PAGE_SIZE: int =
95
HEADER_LINK: str =
'Link'
def
fetch_next_canvas_link(response: requests.models.Response) -> Optional[str]:
17def fetch_next_canvas_link(response: requests.Response) -> typing.Union[str, None]: 18 """ 19 Fetch the Canvas-style next link within the headers. 20 If there is no next link, return None. 21 """ 22 23 headers = response.headers 24 25 if (HEADER_LINK not in headers): 26 return None 27 28 links = headers[HEADER_LINK].split(',') 29 for link in links: 30 parts = link.split(';') 31 if (len(parts) != 2): 32 continue 33 34 if (parts[1].strip() != 'rel="next"'): 35 continue 36 37 return str(parts[0].strip().strip('<>')) 38 39 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]:
41def make_request( 42 method: str, 43 url: str, 44 raise_on_404: bool = False, 45 json: bool = True, 46 **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 47 """ Make a single Canvas request and return the decoded JSON body. """ 48 49 try: 50 _, body_text = edq.net.request.make_request(method, url, **kwargs) 51 except requests.HTTPError as ex: 52 if (raise_on_404 or (ex.response is None) or (ex.response.status_code != http.HTTPStatus.NOT_FOUND)): 53 raise ex 54 55 return None 56 57 if (not json): 58 return body_text 59 60 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]:
62def make_get_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 63 """ Make a single Canvas GET request. """ 64 65 return make_request('GET', url, **kwargs)
Make a single Canvas GET request.
def
make_post_request(url: str, **kwargs: Any) -> Optional[Any]:
67def make_post_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 68 """ Make a single Canvas POST request. """ 69 70 return make_request('POST', url, **kwargs)
Make a single Canvas POST request.
def
make_put_request(url: str, **kwargs: Any) -> Optional[Any]:
72def make_put_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 73 """ Make a single Canvas PUT request. """ 74 75 return make_request('PUT', url, **kwargs)
Make a single Canvas PUT request.
def
make_delete_request(url: str, **kwargs: Any) -> Optional[Any]:
77def make_delete_request(url: str, **kwargs: typing.Any) -> typing.Union[typing.Any, None]: 78 """ Make a single Canvas DELETE request. """ 79 80 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]]]:
82def make_get_request_list( 83 url: str, 84 headers: typing.Dict[str, typing.Any], 85 data: typing.Union[typing.Dict[str, typing.Any], None] = None, 86 raise_on_404: bool = False, 87 ) -> typing.Union[typing.List[typing.Dict[str, typing.Any]], None]: 88 """ Repeatedly call make_get_request() (using a JSON body and next link) until there are no more results. """ 89 90 output: typing.List[typing.Dict[str, typing.Any]] = [] 91 92 next_url: typing.Union[str, None] = url 93 94 while (next_url is not None): 95 try: 96 response, body_text = edq.net.request.make_get(next_url, headers = headers, data = data) 97 except requests.HTTPError as ex: 98 if (raise_on_404 or (ex.response is None) or (ex.response.status_code != http.HTTPStatus.NOT_FOUND)): 99 raise ex 100 101 return None 102 103 next_url = fetch_next_canvas_link(response) 104 105 new_results = edq.util.json.loads(body_text, strict = True) 106 for new_result in new_results: 107 output.append(new_result) 108 109 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]:
111def parse_timestamp(value: typing.Union[str, None]) -> typing.Union[edq.util.time.Timestamp, None]: 112 """ Parse a Canvas-style timestamp into a common form. """ 113 114 if (value is None): 115 return None 116 117 # Parse out some cases that Python <= 3.10 cannot deal with. 118 value = re.sub(r'Z$', '+00:00', value) 119 value = re.sub(r'(\d\d:\d\d)(\.\d+)', r'\1', value) 120 121 pytime = datetime.datetime.fromisoformat(value) 122 return edq.util.time.Timestamp.from_pytime(pytime)
Parse a Canvas-style timestamp into a common form.
def
html_to_markdown(html: Optional[str]) -> str:
124def html_to_markdown(html: typing.Union[str, None]) -> str: 125 """ 126 Parse the text from a Canvas quiz question into markdown. 127 We intend for the resulting markdown to have a little HTML as possible. 128 This is an impossible task, but we want to do our best. 129 """ 130 131 if (html is None): 132 return '' 133 134 converter = html2text.HTML2Text() 135 136 converter.body_width = 0 137 converter.mark_code = True 138 139 text = converter.handle(html) 140 text = text.strip() 141 142 # Replace code tags with fences. 143 text = re.sub(r'\[/?code\]', '```', text) 144 145 # Replace placeholders (e.g., for fill in the blank questions). 146 text = re.sub(r'\[(\w+?)\]', r'<placeholder>\1</placeholder>', text) 147 148 return text
Parse the text from a Canvas quiz question into markdown. We intend for the resulting markdown to have a little HTML as possible. This is an impossible task, but we want to do our best.