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
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.