ff-ltitoolkit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. ff_ltitoolkit-0.1.0.dist-info/METADATA +98 -0
  2. ff_ltitoolkit-0.1.0.dist-info/RECORD +94 -0
  3. ff_ltitoolkit-0.1.0.dist-info/WHEEL +4 -0
  4. ff_ltitoolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
  5. ltitoolkit/__init__.py +20 -0
  6. ltitoolkit/adapters/__init__.py +11 -0
  7. ltitoolkit/adapters/brightspace/__init__.py +35 -0
  8. ltitoolkit/adapters/brightspace/client.py +176 -0
  9. ltitoolkit/adapters/canvas/__init__.py +27 -0
  10. ltitoolkit/adapters/canvas/client.py +142 -0
  11. ltitoolkit/advantage/__init__.py +9 -0
  12. ltitoolkit/advantage/service.py +96 -0
  13. ltitoolkit/core/__init__.py +19 -0
  14. ltitoolkit/core/actions.py +6 -0
  15. ltitoolkit/core/assignments_grades.py +300 -0
  16. ltitoolkit/core/contrib/__init__.py +0 -0
  17. ltitoolkit/core/contrib/django/__init__.py +5 -0
  18. ltitoolkit/core/contrib/django/cookie.py +56 -0
  19. ltitoolkit/core/contrib/django/launch_data_storage/__init__.py +0 -0
  20. ltitoolkit/core/contrib/django/launch_data_storage/cache.py +10 -0
  21. ltitoolkit/core/contrib/django/lti1p3_tool_config/__init__.py +139 -0
  22. ltitoolkit/core/contrib/django/lti1p3_tool_config/admin.py +48 -0
  23. ltitoolkit/core/contrib/django/lti1p3_tool_config/apps.py +6 -0
  24. ltitoolkit/core/contrib/django/lti1p3_tool_config/migrations/0001_initial.py +168 -0
  25. ltitoolkit/core/contrib/django/lti1p3_tool_config/migrations/__init__.py +0 -0
  26. ltitoolkit/core/contrib/django/lti1p3_tool_config/models.py +185 -0
  27. ltitoolkit/core/contrib/django/message_launch.py +39 -0
  28. ltitoolkit/core/contrib/django/oidc_login.py +41 -0
  29. ltitoolkit/core/contrib/django/redirect.py +34 -0
  30. ltitoolkit/core/contrib/django/request.py +32 -0
  31. ltitoolkit/core/contrib/django/session.py +5 -0
  32. ltitoolkit/core/contrib/flask/__init__.py +7 -0
  33. ltitoolkit/core/contrib/flask/cookie.py +34 -0
  34. ltitoolkit/core/contrib/flask/launch_data_storage/__init__.py +0 -0
  35. ltitoolkit/core/contrib/flask/launch_data_storage/cache.py +9 -0
  36. ltitoolkit/core/contrib/flask/message_launch.py +32 -0
  37. ltitoolkit/core/contrib/flask/oidc_login.py +31 -0
  38. ltitoolkit/core/contrib/flask/redirect.py +34 -0
  39. ltitoolkit/core/contrib/flask/request.py +40 -0
  40. ltitoolkit/core/contrib/flask/session.py +5 -0
  41. ltitoolkit/core/contrib/py.typed +0 -0
  42. ltitoolkit/core/cookie.py +17 -0
  43. ltitoolkit/core/cookies_allowed_check.py +151 -0
  44. ltitoolkit/core/course_groups.py +115 -0
  45. ltitoolkit/core/deep_link.py +100 -0
  46. ltitoolkit/core/deep_link_resource.py +96 -0
  47. ltitoolkit/core/deployment.py +13 -0
  48. ltitoolkit/core/exception.py +16 -0
  49. ltitoolkit/core/grade.py +143 -0
  50. ltitoolkit/core/launch_data_storage/__init__.py +0 -0
  51. ltitoolkit/core/launch_data_storage/base.py +75 -0
  52. ltitoolkit/core/launch_data_storage/cache.py +43 -0
  53. ltitoolkit/core/launch_data_storage/session.py +29 -0
  54. ltitoolkit/core/lineitem.py +205 -0
  55. ltitoolkit/core/message_launch.py +828 -0
  56. ltitoolkit/core/message_validators/__init__.py +13 -0
  57. ltitoolkit/core/message_validators/abstract.py +25 -0
  58. ltitoolkit/core/message_validators/deep_link.py +34 -0
  59. ltitoolkit/core/message_validators/privacy_launch.py +40 -0
  60. ltitoolkit/core/message_validators/resource_message.py +21 -0
  61. ltitoolkit/core/message_validators/submission_review.py +45 -0
  62. ltitoolkit/core/names_roles.py +97 -0
  63. ltitoolkit/core/oidc_login.py +275 -0
  64. ltitoolkit/core/py.typed +0 -0
  65. ltitoolkit/core/redirect.py +24 -0
  66. ltitoolkit/core/registration.py +119 -0
  67. ltitoolkit/core/request.py +17 -0
  68. ltitoolkit/core/roles.py +109 -0
  69. ltitoolkit/core/service_connector.py +144 -0
  70. ltitoolkit/core/session.py +70 -0
  71. ltitoolkit/core/tool_config/__init__.py +4 -0
  72. ltitoolkit/core/tool_config/abstract.py +117 -0
  73. ltitoolkit/core/tool_config/dict.py +253 -0
  74. ltitoolkit/core/tool_config/json_file.py +100 -0
  75. ltitoolkit/core/tool_config/py.typed +0 -0
  76. ltitoolkit/core/utils.py +10 -0
  77. ltitoolkit/dynamic_registration/__init__.py +39 -0
  78. ltitoolkit/dynamic_registration/models.py +192 -0
  79. ltitoolkit/dynamic_registration/service.py +156 -0
  80. ltitoolkit/dynamic_registration/store.py +40 -0
  81. ltitoolkit/dynamic_registration/tool_conf.py +102 -0
  82. ltitoolkit/exceptions.py +42 -0
  83. ltitoolkit/fastapi/__init__.py +30 -0
  84. ltitoolkit/fastapi/cookie.py +53 -0
  85. ltitoolkit/fastapi/dynamic_registration.py +40 -0
  86. ltitoolkit/fastapi/message_launch.py +60 -0
  87. ltitoolkit/fastapi/oidc_login.py +47 -0
  88. ltitoolkit/fastapi/redirect.py +54 -0
  89. ltitoolkit/fastapi/request.py +77 -0
  90. ltitoolkit/fastapi/session.py +13 -0
  91. ltitoolkit/http.py +80 -0
  92. ltitoolkit/token/__init__.py +20 -0
  93. ltitoolkit/token/cache.py +47 -0
  94. ltitoolkit/token/service.py +165 -0
@@ -0,0 +1,40 @@
1
+ from flask import request # type: ignore
2
+ from flask import session as flask_session
3
+ from ltitoolkit.core.request import Request
4
+
5
+
6
+ class FlaskRequest(Request):
7
+ _cookies = None
8
+ _request_data = None
9
+ _request_is_secure = None
10
+ _session = None
11
+
12
+ def __init__(
13
+ self, cookies=None, session=None, request_data=None, request_is_secure=None
14
+ ):
15
+ super().__init__()
16
+ self._cookies = request.cookies if cookies is None else cookies
17
+ self._session = flask_session if session is None else session
18
+ self._request_is_secure = (
19
+ request.is_secure if request_is_secure is None else request_is_secure
20
+ )
21
+
22
+ if request_data:
23
+ self._request_data = request_data
24
+
25
+ @property
26
+ def session(self):
27
+ return self._session
28
+
29
+ def get_param(self, key):
30
+ if self._request_data:
31
+ return self._request_data.get(key)
32
+ if request.method == "GET":
33
+ return request.args.get(key, None)
34
+ return request.form.get(key, None)
35
+
36
+ def get_cookie(self, key):
37
+ return self._cookies.get(key)
38
+
39
+ def is_secure(self):
40
+ return self._request_is_secure
@@ -0,0 +1,5 @@
1
+ from ltitoolkit.core.session import SessionService
2
+
3
+
4
+ class FlaskSessionService(SessionService):
5
+ pass
File without changes
@@ -0,0 +1,17 @@
1
+ import typing as t
2
+ from abc import ABCMeta, abstractmethod
3
+
4
+
5
+ class CookieService:
6
+ __metaclass__ = ABCMeta
7
+ _cookie_prefix: str = "lti1p3"
8
+
9
+ @abstractmethod
10
+ def get_cookie(self, name: str) -> t.Optional[str]:
11
+ raise NotImplementedError
12
+
13
+ @abstractmethod
14
+ def set_cookie(
15
+ self, name: str, value: t.Union[str, int], exp: t.Optional[int] = 3600
16
+ ):
17
+ raise NotImplementedError
@@ -0,0 +1,151 @@
1
+ from html import escape # type: ignore
2
+ import json
3
+ import typing as t
4
+
5
+
6
+ class CookiesAllowedCheckPage:
7
+ _params: t.Mapping[str, str] = {}
8
+ _protocol: str = "http"
9
+ _main_text: str = ""
10
+ _click_text: str = ""
11
+ _loading_text: str = ""
12
+
13
+ def __init__(
14
+ self,
15
+ params: t.Mapping[str, str],
16
+ protocol: str,
17
+ main_text: str,
18
+ click_text: str,
19
+ loading_text: str,
20
+ *args,
21
+ **kwargs
22
+ ):
23
+ # pylint: disable=unused-argument
24
+ self._params = params
25
+ self._protocol = protocol
26
+ self._main_text = main_text
27
+ self._click_text = click_text
28
+ self._loading_text = loading_text
29
+
30
+ def get_css_block(self) -> str:
31
+ css_block = """\
32
+ body {
33
+ font-family: Geneva, Arial, Helvetica, sans-serif;
34
+ }
35
+ """
36
+ return css_block
37
+
38
+ def get_js_block(self) -> str:
39
+ js_block = """\
40
+ var siteProtocol = '%s';
41
+ var urlParams = %s;
42
+ var htmlEntities = {
43
+ "&lt;": "<",
44
+ "&gt;": ">",
45
+ "&amp;": "&",
46
+ "&quot;": '"',
47
+ "&#x27;": "'"
48
+ };
49
+
50
+ function unescapeHtmlEntities(str) {
51
+ for (var htmlCode in htmlEntities) {
52
+ str = str.replace(new RegExp(htmlCode, "g"), htmlEntities[htmlCode]);
53
+ }
54
+ return str;
55
+ }
56
+
57
+ function getUpdatedUrl() {
58
+ var newSearchParams = [];
59
+ for (var key in urlParams) {
60
+ if (window.location.search.indexOf(key + '=') === -1) {
61
+ newSearchParams.push(key + '=' + encodeURIComponent(unescapeHtmlEntities(urlParams[key])));
62
+ }
63
+ }
64
+ var searchParamsStr = newSearchParams.join('&');
65
+ if (window.location.search !== '') {
66
+ searchParamsStr = window.location.search + '&' + searchParamsStr;
67
+ } else {
68
+ searchParamsStr = '?' + searchParamsStr;
69
+ }
70
+ return window.location.protocol + '//' + window.location.hostname +
71
+ (window.location.port ? (":" + window.location.port) : "") +
72
+ window.location.pathname + searchParamsStr;
73
+ }
74
+
75
+ function displayLoadingBlock() {
76
+ document.getElementById("lti1p3-loading-msg").style.display = "block";
77
+ }
78
+
79
+ function displayWarningBlock() {
80
+ document.getElementById("lti1p3-warning-msg").style.display = "block";
81
+ var newTabLink = document.getElementById("lti1p3-new-tab-link");
82
+ var contentUrl = getUpdatedUrl();
83
+ newTabLink.onclick = function() {
84
+ window.open(contentUrl , '_blank');
85
+ newTabLink.parentNode.removeChild(newTabLink);
86
+ };
87
+ }
88
+
89
+ function checkCookiesAllowed() {
90
+ var cookie = "lti1p3_test_cookie=1; path=/";
91
+ if (siteProtocol === 'https') {
92
+ cookie = cookie + '; SameSite=None; secure';
93
+ }
94
+ document.cookie = cookie;
95
+ var res = document.cookie.indexOf("lti1p3_test_cookie") !== -1;
96
+ if (res) {
97
+ // remove test cookie and reload page
98
+ document.cookie = "lti1p3_test_cookie=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
99
+ displayLoadingBlock();
100
+ window.location.href = getUpdatedUrl();
101
+ } else {
102
+ displayWarningBlock();
103
+ }
104
+ }
105
+
106
+ document.addEventListener("DOMContentLoaded", checkCookiesAllowed);
107
+ """
108
+ # pylint: disable=deprecated-method
109
+ js_block = js_block % (
110
+ self._protocol,
111
+ json.dumps({k: escape(v, True) for k, v in self._params.items()}),
112
+ )
113
+ return js_block
114
+
115
+ def get_header_block(self) -> str:
116
+ return ""
117
+
118
+ def get_html(self) -> str:
119
+ html = """\
120
+ <!DOCTYPE html>
121
+ <html lang="en">
122
+ <head>
123
+ <title></title>
124
+ <meta charset="UTF-8">
125
+ <style type="text/css">
126
+ {css_block}
127
+ </style>
128
+ <script type="text/javascript">
129
+ {js_block}
130
+ </script>
131
+ </head>
132
+ <body>
133
+ <div id="lti1p3-loading-msg" style="display: none;">
134
+ {loading_text}
135
+ </div>
136
+ <div id="lti1p3-warning-msg" style="display: none;">
137
+ {header_block}
138
+ <p><strong>{main_text}</strong> <a href="javascript: void(0);" id="lti1p3-new-tab-link">{click_text}</a></p>
139
+ </div>
140
+ </body>
141
+ </html>
142
+ """
143
+ html = html.format(
144
+ css_block=self.get_css_block(),
145
+ js_block=self.get_js_block(),
146
+ loading_text=self._loading_text,
147
+ header_block=self.get_header_block(),
148
+ main_text=self._main_text,
149
+ click_text=self._click_text,
150
+ )
151
+ return html
@@ -0,0 +1,115 @@
1
+ import typing as t
2
+ import typing_extensions as te
3
+ from .utils import add_param_to_url
4
+ from .service_connector import ServiceConnector
5
+
6
+ TGroupsServiceData = te.TypedDict(
7
+ "TGroupsServiceData",
8
+ {
9
+ # Required data
10
+ "context_groups_url": str,
11
+ "scope": t.List[
12
+ te.Literal[
13
+ "https://purl.imsglobal.org/spec/lti-gs/scope/contextgroup.readonly"
14
+ ]
15
+ ],
16
+ "service_versions": t.List[str],
17
+ # Optional data
18
+ "context_group_sets_url": str,
19
+ },
20
+ total=False,
21
+ )
22
+
23
+ TGroup = te.TypedDict(
24
+ "TGroup",
25
+ {
26
+ # Required data
27
+ "id": t.Union[str, int],
28
+ "name": str,
29
+ # Optional data
30
+ "tag": str,
31
+ "set_id": t.Union[str, int],
32
+ },
33
+ total=False,
34
+ )
35
+
36
+ TSet = te.TypedDict(
37
+ "TSet",
38
+ {
39
+ # Required data
40
+ "id": t.Union[str, int],
41
+ "name": str,
42
+ # Optional data
43
+ "groups": t.List[TGroup],
44
+ },
45
+ total=False,
46
+ )
47
+
48
+
49
+ class CourseGroupsService:
50
+ _service_connector: ServiceConnector
51
+ _service_data: TGroupsServiceData
52
+
53
+ def __init__(
54
+ self,
55
+ service_connector: ServiceConnector,
56
+ groups_service_data: TGroupsServiceData,
57
+ ):
58
+ self._service_connector = service_connector
59
+ self._service_data = groups_service_data
60
+
61
+ def get_page(
62
+ self, data_url: str, data_key: str = "groups"
63
+ ) -> t.Tuple[list, t.Optional[str]]:
64
+ """
65
+ Get one page with the groups/sets.
66
+
67
+ :param data_url
68
+ :param data_key
69
+ :return: tuple in format: (list with data items, next page url)
70
+ """
71
+ data = self._service_connector.make_service_request(
72
+ self._service_data["scope"],
73
+ data_url,
74
+ accept="application/vnd.ims.lti-gs.v1.contextgroupcontainer+json",
75
+ )
76
+ data_body = t.cast(t.Any, data.get("body", {}))
77
+ return data_body.get(data_key, []), data["next_page_url"]
78
+
79
+ def get_groups(self, user_id=None):
80
+ groups_res_lst = []
81
+ groups_url = self._service_data.get("context_groups_url")
82
+ if user_id:
83
+ groups_url = add_param_to_url(groups_url, "user_id", user_id)
84
+
85
+ while groups_url:
86
+ groups, groups_url = self.get_page(groups_url, data_key="groups")
87
+ groups_res_lst.extend(groups)
88
+
89
+ return groups_res_lst
90
+
91
+ def has_sets(self):
92
+ return "context_group_sets_url" in self._service_data
93
+
94
+ def get_sets(self, include_groups=False):
95
+ sets_res_lst = []
96
+ sets_url = self._service_data.get("context_group_sets_url")
97
+
98
+ while sets_url:
99
+ sets, sets_url = self.get_page(sets_url, data_key="sets")
100
+ sets_res_lst.extend(sets)
101
+
102
+ if include_groups and sets_res_lst:
103
+ set_id_to_index = {}
104
+ for i, s in enumerate(sets_res_lst):
105
+ set_id_to_index[s["id"]] = i
106
+ sets_res_lst[i]["groups"] = []
107
+
108
+ groups = self.get_groups()
109
+ for group in groups:
110
+ set_id = group.get("set_id")
111
+ if set_id and set_id in set_id_to_index:
112
+ index = set_id_to_index[set_id]
113
+ sets_res_lst[index]["groups"].append(group)
114
+
115
+ return sets_res_lst
@@ -0,0 +1,100 @@
1
+ import time
2
+ import typing as t
3
+ import uuid
4
+
5
+ import jwt # type: ignore
6
+ import typing_extensions as te
7
+ from .deep_link_resource import DeepLinkResource
8
+ from .registration import Registration
9
+
10
+ TDeepLinkData = te.TypedDict(
11
+ "TDeepLinkData",
12
+ {
13
+ # Required data:
14
+ "deep_link_return_url": str,
15
+ "accept_types": t.List[te.Literal["link", "ltiResourceLink"]],
16
+ "accept_presentation_document_targets": t.List[
17
+ te.Literal["iframe", "window", "embed"]
18
+ ],
19
+ # Optional data
20
+ "accept_multiple": t.Union[bool, te.Literal["true", "false"]],
21
+ "auto_create": t.Union[bool, te.Literal["true", "false"]],
22
+ "title": str,
23
+ "text": str,
24
+ "data": object,
25
+ },
26
+ total=False,
27
+ )
28
+
29
+
30
+ class DeepLink:
31
+ _registration: Registration
32
+ _deployment_id: str
33
+ _deep_link_settings: TDeepLinkData
34
+
35
+ def __init__(
36
+ self,
37
+ registration: Registration,
38
+ deployment_id: str,
39
+ deep_link_settings: TDeepLinkData,
40
+ ):
41
+ self._registration = registration
42
+ self._deployment_id = deployment_id
43
+ self._deep_link_settings = deep_link_settings
44
+
45
+ def _generate_nonce(self):
46
+ return uuid.uuid4().hex + uuid.uuid1().hex
47
+
48
+ def get_message_jwt(
49
+ self, resources: t.Sequence[DeepLinkResource]
50
+ ) -> t.Dict[str, object]:
51
+ message_jwt = {
52
+ "iss": self._registration.get_client_id(),
53
+ "aud": [self._registration.get_issuer()],
54
+ "exp": int(time.time()) + 600,
55
+ "iat": int(time.time()),
56
+ "nonce": "nonce-" + self._generate_nonce(),
57
+ "https://purl.imsglobal.org/spec/lti/claim/deployment_id": self._deployment_id,
58
+ "https://purl.imsglobal.org/spec/lti/claim/message_type": "LtiDeepLinkingResponse",
59
+ "https://purl.imsglobal.org/spec/lti/claim/version": "1.3.0",
60
+ "https://purl.imsglobal.org/spec/lti-dl/claim/content_items": [
61
+ r.to_dict() for r in resources
62
+ ],
63
+ "https://purl.imsglobal.org/spec/lti-dl/claim/data": self._deep_link_settings.get(
64
+ "data"
65
+ ),
66
+ }
67
+ return message_jwt
68
+
69
+ def encode_jwt(self, message):
70
+ headers = None
71
+ kid = self._registration.get_kid()
72
+ if kid:
73
+ headers = {"kid": kid}
74
+ encoded_jwt = jwt.encode(
75
+ message,
76
+ self._registration.get_tool_private_key(),
77
+ algorithm="RS256",
78
+ headers=headers,
79
+ )
80
+ if isinstance(encoded_jwt, bytes):
81
+ return encoded_jwt.decode("utf-8")
82
+ return encoded_jwt
83
+
84
+ def get_response_jwt(self, resources: t.Sequence[DeepLinkResource]) -> str:
85
+ message_jwt = self.get_message_jwt(resources)
86
+ return self.encode_jwt(message_jwt)
87
+
88
+ def get_response_form_html(self, jwt_val: str) -> str:
89
+ deep_link_return_url = self._deep_link_settings["deep_link_return_url"]
90
+ html = (
91
+ f'<form id="lti13_deep_link_auto_submit" action="{deep_link_return_url}" method="POST">'
92
+ f'<input type="hidden" name="JWT" value="{jwt_val}" /></form>'
93
+ f"<script type=\"text/javascript\">document.getElementById('lti13_deep_link_auto_submit').submit();"
94
+ f"</script>"
95
+ )
96
+ return html
97
+
98
+ def output_response_form(self, resources: t.List[DeepLinkResource]) -> str:
99
+ jwt_val = self.get_response_jwt(resources)
100
+ return self.get_response_form_html(jwt_val)
@@ -0,0 +1,96 @@
1
+ import typing as t
2
+ from .lineitem import LineItem
3
+
4
+
5
+ class DeepLinkResource:
6
+ _type: str = "ltiResourceLink"
7
+ _title: t.Optional[str] = None
8
+ _url: t.Optional[str] = None
9
+ _lineitem: t.Optional[LineItem] = None
10
+ _custom_params: t.Mapping[str, str] = {}
11
+ _target: str = "iframe"
12
+ _icon_url: t.Optional[str] = None
13
+
14
+ def get_type(self):
15
+ return self._type
16
+
17
+ def set_type(self, value: str) -> "DeepLinkResource":
18
+ self._type = value
19
+ return self
20
+
21
+ def get_title(self) -> t.Optional[str]:
22
+ return self._title
23
+
24
+ def set_title(self, value: str) -> "DeepLinkResource":
25
+ self._title = value
26
+ return self
27
+
28
+ def get_url(self) -> t.Optional[str]:
29
+ return self._url
30
+
31
+ def set_url(self, value: str) -> "DeepLinkResource":
32
+ self._url = value
33
+ return self
34
+
35
+ def get_lineitem(self) -> t.Optional[LineItem]:
36
+ return self._lineitem
37
+
38
+ def set_lineitem(self, value: LineItem) -> "DeepLinkResource":
39
+ self._lineitem = value
40
+ return self
41
+
42
+ def get_custom_params(self) -> t.Mapping[str, str]:
43
+ return self._custom_params
44
+
45
+ def set_custom_params(self, value: t.Mapping[str, str]) -> "DeepLinkResource":
46
+ self._custom_params = value
47
+ return self
48
+
49
+ def get_target(self) -> str:
50
+ return self._target
51
+
52
+ def set_target(self, value: str) -> "DeepLinkResource":
53
+ self._target = value
54
+ return self
55
+
56
+ def get_icon_url(self) -> t.Optional[str]:
57
+ return self._icon_url
58
+
59
+ def set_icon_url(self, value: str) -> "DeepLinkResource":
60
+ self._icon_url = value
61
+ return self
62
+
63
+ def to_dict(self) -> t.Dict[str, object]:
64
+ res: t.Dict[str, object] = {
65
+ "type": self._type,
66
+ "title": self._title,
67
+ "url": self._url,
68
+ "custom": self._custom_params,
69
+ }
70
+ if self._lineitem:
71
+ line_item: t.Dict[str, object] = {
72
+ "scoreMaximum": self._lineitem.get_score_maximum(),
73
+ }
74
+
75
+ label = self._lineitem.get_label()
76
+ if label:
77
+ line_item["label"] = label
78
+
79
+ resource_id = self._lineitem.get_resource_id()
80
+ if resource_id:
81
+ line_item["resourceId"] = resource_id
82
+
83
+ tag = self._lineitem.get_tag()
84
+ if tag:
85
+ line_item["tag"] = tag
86
+
87
+ submission_review = self._lineitem.get_submission_review()
88
+ if submission_review:
89
+ line_item["submissionReview"] = submission_review
90
+
91
+ res["lineItem"] = line_item
92
+
93
+ if self._icon_url:
94
+ res["icon"] = {"url": self._icon_url}
95
+
96
+ return res
@@ -0,0 +1,13 @@
1
+ import typing as t
2
+
3
+
4
+ class Deployment:
5
+
6
+ _deployment_id: t.Optional[str] = None
7
+
8
+ def get_deployment_id(self) -> t.Optional[str]:
9
+ return self._deployment_id
10
+
11
+ def set_deployment_id(self, deployment_id: str) -> "Deployment":
12
+ self._deployment_id = deployment_id
13
+ return self
@@ -0,0 +1,16 @@
1
+ import requests
2
+
3
+
4
+ class LtiException(Exception):
5
+ pass
6
+
7
+
8
+ class OIDCException(Exception):
9
+ pass
10
+
11
+
12
+ class LtiServiceException(LtiException):
13
+ def __init__(self, response: requests.Response):
14
+ msg = f"HTTP response [{response.url}]: {str(response.status_code)} - {response.text}"
15
+ super().__init__(msg)
16
+ self.response = response