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,143 @@
1
+ import json
2
+ import typing as t
3
+ from .exception import LtiException
4
+
5
+
6
+ TExtaClaims = t.Mapping[str, t.Any]
7
+
8
+
9
+ class Grade:
10
+ _score_given: t.Optional[float] = None
11
+ _score_maximum: t.Optional[float] = None
12
+ _activity_progress: t.Optional[str] = None
13
+ _grading_progress: t.Optional[str] = None
14
+ _timestamp: t.Optional[str] = None
15
+ _user_id: t.Optional[str] = None
16
+ _comment: t.Optional[str] = None
17
+ _extra_claims: t.Optional[TExtaClaims] = None
18
+
19
+ def _validate_score(self, score_value) -> t.Optional[str]:
20
+ if not isinstance(score_value, (int, float)):
21
+ return "score must be integer or float"
22
+ if score_value < 0:
23
+ return "score must be positive number (including 0)"
24
+ return None
25
+
26
+ def get_score_given(self) -> t.Optional[float]:
27
+ """
28
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#scoregiven-and-scoremaximum
29
+ """
30
+ return self._score_given
31
+
32
+ def set_score_given(self, value: float) -> "Grade":
33
+ """
34
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#scoregiven-and-scoremaximum
35
+ """
36
+ err_msg = self._validate_score(value)
37
+ if err_msg is not None:
38
+ raise LtiException("Invalid scoreGiven value: " + err_msg)
39
+ self._score_given = value
40
+ return self
41
+
42
+ def get_score_maximum(self) -> t.Optional[float]:
43
+ """
44
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#scoregiven-and-scoremaximum
45
+ """
46
+ return self._score_maximum
47
+
48
+ def set_score_maximum(self, value: float) -> "Grade":
49
+ """
50
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#scoregiven-and-scoremaximum
51
+ """
52
+ err_msg = self._validate_score(value)
53
+ if err_msg is not None:
54
+ raise LtiException("Invalid scoreMaximum value: " + err_msg)
55
+ self._score_maximum = value
56
+ return self
57
+
58
+ def get_activity_progress(self) -> t.Optional[str]:
59
+ """
60
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#activityprogress
61
+ """
62
+ return self._activity_progress
63
+
64
+ def set_activity_progress(self, value: str) -> "Grade":
65
+ """
66
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#activityprogress
67
+ """
68
+ self._activity_progress = value
69
+ return self
70
+
71
+ def get_grading_progress(self) -> t.Optional[str]:
72
+ """
73
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#gradingprogress
74
+ """
75
+ return self._grading_progress
76
+
77
+ def set_grading_progress(self, value: str) -> "Grade":
78
+ """
79
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#gradingprogress
80
+ """
81
+ self._grading_progress = value
82
+ return self
83
+
84
+ def get_timestamp(self) -> t.Optional[str]:
85
+ """
86
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#timestamp
87
+ """
88
+ return self._timestamp
89
+
90
+ def set_timestamp(self, value: str) -> "Grade":
91
+ """
92
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#timestamp
93
+ """
94
+ self._timestamp = value
95
+ return self
96
+
97
+ def get_user_id(self) -> t.Optional[str]:
98
+ """
99
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#userid-0
100
+ """
101
+ return self._user_id
102
+
103
+ def set_user_id(self, value: str) -> "Grade":
104
+ """
105
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#userid-0
106
+ """
107
+ self._user_id = value
108
+ return self
109
+
110
+ def get_comment(self) -> t.Optional[str]:
111
+ """
112
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#comment-0
113
+ """
114
+ return self._comment
115
+
116
+ def set_comment(self, value: str) -> "Grade":
117
+ """
118
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#comment-0
119
+ """
120
+ self._comment = value
121
+ return self
122
+
123
+ def set_extra_claims(self, value: TExtaClaims) -> "Grade":
124
+ self._extra_claims = value
125
+ return self
126
+
127
+ def get_extra_claims(self) -> t.Optional[TExtaClaims]:
128
+ return self._extra_claims
129
+
130
+ def get_value(self) -> str:
131
+ data = {
132
+ "scoreGiven": self._score_given,
133
+ "scoreMaximum": self._score_maximum,
134
+ "activityProgress": self._activity_progress,
135
+ "gradingProgress": self._grading_progress,
136
+ "timestamp": self._timestamp,
137
+ "userId": self._user_id,
138
+ "comment": self._comment,
139
+ }
140
+ if self._extra_claims is not None:
141
+ data.update(self._extra_claims)
142
+
143
+ return json.dumps({k: v for k, v in data.items() if v is not None})
File without changes
@@ -0,0 +1,75 @@
1
+ import typing as t
2
+ from abc import ABCMeta, abstractmethod
3
+ from ..request import Request
4
+
5
+ T = t.TypeVar("T")
6
+
7
+
8
+ class LaunchDataStorage(t.Generic[T]):
9
+ __metaclass__ = ABCMeta
10
+ _request: t.Optional[Request] = None
11
+ _session_id: t.Optional[str] = None
12
+ _session_cookie_name: str = "session-id"
13
+ _prefix: str = "lti1p3-"
14
+
15
+ def __init__(self, *args, **kwargs) -> None:
16
+ pass
17
+
18
+ def set_request(self, request: Request) -> None:
19
+ self._request = request
20
+
21
+ def get_session_cookie_name(self) -> t.Optional[str]:
22
+ return self._session_cookie_name
23
+
24
+ def get_session_id(self) -> t.Optional[str]:
25
+ return self._session_id
26
+
27
+ def set_session_id(self, session_id: str) -> None:
28
+ self._session_id = session_id
29
+
30
+ def remove_session_id(self) -> None:
31
+ self._session_id = None
32
+
33
+ def _prepare_key(self, key: str) -> str:
34
+ if self._session_id:
35
+ if key.startswith(self._prefix):
36
+ key = key[len(self._prefix) :]
37
+ return self._prefix + self._session_id + "-" + key
38
+ if not key.startswith(self._prefix):
39
+ key = self._prefix + key
40
+ return key
41
+
42
+ @abstractmethod
43
+ def can_set_keys_expiration(self) -> bool:
44
+ raise NotImplementedError
45
+
46
+ @abstractmethod
47
+ def get_value(self, key: str) -> T:
48
+ raise NotImplementedError
49
+
50
+ @abstractmethod
51
+ def set_value(self, key: str, value: T, exp: t.Optional[int] = None) -> None:
52
+ raise NotImplementedError
53
+
54
+ @abstractmethod
55
+ def check_value(self, key: str) -> bool:
56
+ raise NotImplementedError
57
+
58
+
59
+ class DisableSessionId:
60
+ _session_id: t.Optional[str] = None
61
+ _launch_data_storage: t.Optional[LaunchDataStorage] = None
62
+
63
+ def __init__(self, launch_data_storage: t.Optional[LaunchDataStorage]) -> None:
64
+ self._launch_data_storage = launch_data_storage
65
+ if launch_data_storage:
66
+ self._session_id = launch_data_storage.get_session_id()
67
+
68
+ def __enter__(self) -> "DisableSessionId":
69
+ if self._launch_data_storage:
70
+ self._launch_data_storage.remove_session_id()
71
+ return self
72
+
73
+ def __exit__(self, *args) -> None:
74
+ if self._launch_data_storage and self._session_id:
75
+ self._launch_data_storage.set_session_id(self._session_id)
@@ -0,0 +1,43 @@
1
+ import typing as t
2
+
3
+ from .base import LaunchDataStorage
4
+
5
+ T = t.TypeVar("T")
6
+
7
+
8
+ class CacheDataStorage(LaunchDataStorage[T], t.Generic[T]):
9
+ _cache = None
10
+
11
+ def get_session_cookie_name(self) -> t.Optional[str]:
12
+ """
13
+ Workaround for the local non-HTTP usage.
14
+ There is odd situation that all cookies become unavailable from some time
15
+ on the launch step (even if they were set in the new window).
16
+ Looks like it is bug in Chrome >= 80 related to SameSite changes.
17
+ So because of this we have to set all cache values without session prefix.
18
+ It is less secure because if you know unique launch_id you may get access to launch data,
19
+ but unfortunately there is no other way. So please use HTTPS on production :-)
20
+ """
21
+ assert self._request is not None, "Request should be set at this point"
22
+ if not self._request.is_secure():
23
+ return None
24
+ return super().get_session_cookie_name()
25
+
26
+ def _get_cache(self):
27
+ assert self._cache is not None, "Cache is not set"
28
+ return self._cache
29
+
30
+ def get_value(self, key) -> T:
31
+ key = self._prepare_key(key)
32
+ return self._get_cache().get(key)
33
+
34
+ def set_value(self, key: str, value: T, exp: t.Optional[int] = None) -> None:
35
+ key = self._prepare_key(key)
36
+ self._get_cache().set(key, value, exp)
37
+
38
+ def check_value(self, key: str) -> bool:
39
+ key = self._prepare_key(key)
40
+ return self._get_cache().get(key) is not None
41
+
42
+ def can_set_keys_expiration(self) -> bool:
43
+ return True
@@ -0,0 +1,29 @@
1
+ import typing as t
2
+
3
+ from .base import LaunchDataStorage
4
+
5
+ T = t.TypeVar("T")
6
+
7
+
8
+ class SessionDataStorage(LaunchDataStorage[T], t.Generic[T]):
9
+ def get_session_cookie_name(self) -> None:
10
+ return None
11
+
12
+ def set_session_id(self, session_id: str) -> None:
13
+ pass
14
+
15
+ def get_value(self, key: str) -> T:
16
+ assert self._request is not None, "Request should be set at this point"
17
+ return self._request.session.get(key, None)
18
+
19
+ def set_value(self, key: str, value: T, exp: t.Optional[int] = None) -> None:
20
+ # pylint: disable=unused-argument
21
+ assert self._request is not None, "Request should be set at this point"
22
+ self._request.session[key] = value
23
+
24
+ def check_value(self, key: str) -> bool:
25
+ assert self._request is not None, "Request should be set at this point"
26
+ return key in self._request.session
27
+
28
+ def can_set_keys_expiration(self) -> bool:
29
+ return False
@@ -0,0 +1,205 @@
1
+ import json
2
+ import typing as t
3
+ import typing_extensions as te
4
+ from .exception import LtiException
5
+
6
+
7
+ TSubmissionReview = te.TypedDict(
8
+ "TSubmissionReview",
9
+ {
10
+ # Required data
11
+ "reviewableStatus": list,
12
+ # Optional data
13
+ "label": str,
14
+ "url": str,
15
+ "custom": t.Dict[str, str],
16
+ },
17
+ total=False,
18
+ )
19
+
20
+ TLineItem = te.TypedDict(
21
+ "TLineItem",
22
+ {
23
+ "id": str,
24
+ "scoreMaximum": int,
25
+ "label": str,
26
+ "resourceId": str,
27
+ "tag": str,
28
+ "resourceLinkId": str,
29
+ "startDateTime": str,
30
+ "endDateTime": str,
31
+ "submissionReview": TSubmissionReview,
32
+ },
33
+ total=False,
34
+ )
35
+
36
+
37
+ class LineItem:
38
+ _id: t.Optional[str] = None
39
+ _score_maximum: t.Optional[float] = None
40
+ _label: t.Optional[str] = None
41
+ _resource_id: t.Optional[str] = None
42
+ _resource_link_id: t.Optional[str] = None
43
+ _tag: t.Optional[str] = None
44
+ _start_date_time: t.Optional[str] = None
45
+ _end_date_time: t.Optional[str] = None
46
+ _submission_review: t.Optional[TSubmissionReview] = None
47
+
48
+ def __init__(self, lineitem: t.Optional[TLineItem] = None):
49
+ if not lineitem:
50
+ lineitem = {}
51
+ self._id = lineitem.get("id")
52
+ self._score_maximum = lineitem.get("scoreMaximum")
53
+ self._label = lineitem.get("label")
54
+ self._resource_id = lineitem.get("resourceId")
55
+ self._resource_link_id = lineitem.get("resourceLinkId")
56
+ self._tag = lineitem.get("tag")
57
+ self._start_date_time = lineitem.get("startDateTime")
58
+ self._end_date_time = lineitem.get("endDateTime")
59
+ self._submission_review = lineitem.get("submissionReview")
60
+
61
+ def get_id(self) -> t.Optional[str]:
62
+ return self._id
63
+
64
+ def set_id(self, value: str) -> "LineItem":
65
+ self._id = value
66
+ return self
67
+
68
+ def get_label(self) -> t.Optional[str]:
69
+ """
70
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#label
71
+ """
72
+ return self._label
73
+
74
+ def set_label(self, value: str) -> "LineItem":
75
+ """
76
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#label
77
+ """
78
+ self._label = value
79
+ return self
80
+
81
+ def get_score_maximum(self) -> t.Optional[float]:
82
+ """
83
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#scoremaximum
84
+ """
85
+ return self._score_maximum
86
+
87
+ def set_score_maximum(self, value: float) -> "LineItem":
88
+ """
89
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#scoremaximum
90
+ """
91
+ if not isinstance(value, (int, float)):
92
+ raise LtiException(
93
+ "Invalid scoreMaximum value: score must be integer or float"
94
+ )
95
+ if value <= 0:
96
+ raise LtiException(
97
+ "Invalid scoreMaximum value: score must be non null value, strictly greater than 0"
98
+ )
99
+
100
+ self._score_maximum = value
101
+ return self
102
+
103
+ def get_resource_id(self) -> t.Optional[str]:
104
+ """
105
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#tool-resource-identifier-resourceid
106
+ """
107
+ return self._resource_id
108
+
109
+ def set_resource_id(self, value: str) -> "LineItem":
110
+ """
111
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#tool-resource-identifier-resourceid
112
+ """
113
+ self._resource_id = value
114
+ return self
115
+
116
+ def get_resource_link_id(self) -> t.Optional[str]:
117
+ """
118
+ https://www.imsglobal.org/spec/lti-ags/v2p0#resourcelinkid-and-binding-a-line-item-to-a-resource-link
119
+ """
120
+ return self._resource_link_id
121
+
122
+ def set_resource_link_id(self, value: str) -> "LineItem":
123
+ """
124
+ https://www.imsglobal.org/spec/lti-ags/v2p0#resourcelinkid-and-binding-a-line-item-to-a-resource-link
125
+ """
126
+ self._resource_link_id = value
127
+ return self
128
+
129
+ def get_tag(self) -> t.Optional[str]:
130
+ """
131
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#tag
132
+ """
133
+ return self._tag
134
+
135
+ def set_tag(self, value: str) -> "LineItem":
136
+ """
137
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#tag
138
+ """
139
+ self._tag = value
140
+ return self
141
+
142
+ def get_start_date_time(self) -> t.Optional[str]:
143
+ """
144
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#startdatetime
145
+ """
146
+ return self._start_date_time
147
+
148
+ def set_start_date_time(self, value: str) -> "LineItem":
149
+ """
150
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#startdatetime
151
+ """
152
+ self._start_date_time = value
153
+ return self
154
+
155
+ def get_end_date_time(self) -> t.Optional[str]:
156
+ """
157
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#enddatetime
158
+ """
159
+ return self._end_date_time
160
+
161
+ def set_end_date_time(self, value: str) -> "LineItem":
162
+ """
163
+ https://www.imsglobal.org/spec/lti-ags/v2p0/#enddatetime
164
+ """
165
+ self._end_date_time = value
166
+ return self
167
+
168
+ def get_submission_review(self) -> t.Optional[TSubmissionReview]:
169
+ return self._submission_review
170
+
171
+ def set_submission_review(
172
+ self,
173
+ reviewable_status: t.List,
174
+ label: t.Optional[str] = None,
175
+ url: t.Optional[str] = None,
176
+ custom: t.Optional[t.Dict[str, str]] = None,
177
+ ) -> "LineItem":
178
+ if not isinstance(reviewable_status, list):
179
+ raise Exception('Invalid "reviewable_status" argument')
180
+
181
+ self._submission_review: TSubmissionReview = {
182
+ "reviewableStatus": reviewable_status
183
+ }
184
+ if label:
185
+ self._submission_review["label"] = label
186
+ if url:
187
+ self._submission_review["url"] = url
188
+ if custom:
189
+ self._submission_review["custom"] = custom
190
+
191
+ return self
192
+
193
+ def get_value(self) -> str:
194
+ data = {
195
+ "id": self._id if self._id else None,
196
+ "scoreMaximum": self._score_maximum,
197
+ "label": self._label,
198
+ "resourceId": self._resource_id,
199
+ "resourceLinkId": self._resource_link_id,
200
+ "tag": self._tag,
201
+ "startDateTime": self._start_date_time,
202
+ "endDateTime": self._end_date_time,
203
+ "submissionReview": self._submission_review,
204
+ }
205
+ return json.dumps({k: v for k, v in data.items() if v})