canvas 0.25.0__py3-none-any.whl → 0.27.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.
Potentially problematic release.
This version of canvas might be problematic. Click here for more details.
- {canvas-0.25.0.dist-info → canvas-0.27.0.dist-info}/METADATA +1 -1
- {canvas-0.25.0.dist-info → canvas-0.27.0.dist-info}/RECORD +11 -11
- canvas_sdk/handlers/simple_api/api.py +193 -28
- canvas_sdk/handlers/simple_api/tools.py +115 -0
- canvas_sdk/tests/handlers/test_simple_api.py +191 -33
- canvas_sdk/v1/data/__init__.py +4 -2
- canvas_sdk/v1/data/appointment.py +22 -0
- canvas_sdk/v1/data/staff.py +25 -1
- plugin_runner/sandbox.py +1 -0
- plugin_runner/tests/data/plugins/.gitkeep +0 -0
- {canvas-0.25.0.dist-info → canvas-0.27.0.dist-info}/WHEEL +0 -0
- {canvas-0.25.0.dist-info → canvas-0.27.0.dist-info}/entry_points.txt +0 -0
|
@@ -193,9 +193,10 @@ canvas_sdk/handlers/application.py,sha256=6Y18g65ae1ws7-llyKzyWipOg9Y8zVEoV7Y_0E
|
|
|
193
193
|
canvas_sdk/handlers/base.py,sha256=CpUIDtnZJCJTHytXuz_x43NU8Zy1ejorqfGf0e2H2oY,1422
|
|
194
194
|
canvas_sdk/handlers/cron_task.py,sha256=zShv4qGUmpUjG3HrXVqzMir9kZyqo4nDT3ETqNP13sk,954
|
|
195
195
|
canvas_sdk/handlers/simple_api/__init__.py,sha256=4_GLVafEsgdIBGyxJ6lpl2V0d--MHPsBQrdOeun7o98,431
|
|
196
|
-
canvas_sdk/handlers/simple_api/api.py,sha256=
|
|
196
|
+
canvas_sdk/handlers/simple_api/api.py,sha256=ciIKKNYKeDqRm3vOCXRSZBoIjP-l-V4MM3EBExnDY_Q,18974
|
|
197
197
|
canvas_sdk/handlers/simple_api/exceptions.py,sha256=qSm_TnN3fdHZf5wz8BS3ueWpjyCDDY2yik0wx2HhGrc,1245
|
|
198
198
|
canvas_sdk/handlers/simple_api/security.py,sha256=_jWcJv9xTIeCz8B5vYPm3U8rwhDmFtjSaa5jKpM4VQw,5601
|
|
199
|
+
canvas_sdk/handlers/simple_api/tools.py,sha256=o5Dtwj2i7A-W7owxqhl1Yn8tBtnKB_fsJ9aMiouNYeU,3961
|
|
199
200
|
canvas_sdk/protocols/__init__.py,sha256=3u9zet5D4DX4V953tLCoN1xhaOhAUCwGwscMv-7IIxo,186
|
|
200
201
|
canvas_sdk/protocols/base.py,sha256=sbm0uOk3PPfPemqBmHh2hawE5utC6no46EmvyMN8Y7Q,179
|
|
201
202
|
canvas_sdk/protocols/clinical_quality_measure.py,sha256=8cU93ah9YsPecpZR1-csAbg69oFn9a8LtjHjYMMHedw,4844
|
|
@@ -210,7 +211,7 @@ canvas_sdk/templates/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
|
|
|
210
211
|
canvas_sdk/templates/tests/test_utils.py,sha256=VRahmmVwXKcp1NMLoA3BZL4cFFXzFnD-i5IUpcEeXTg,1832
|
|
211
212
|
canvas_sdk/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
212
213
|
canvas_sdk/tests/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
213
|
-
canvas_sdk/tests/handlers/test_simple_api.py,sha256=
|
|
214
|
+
canvas_sdk/tests/handlers/test_simple_api.py,sha256=Uh6tbazyTB7bYVqACV1M3eGzrlN8bx-c67b46ghd4e0,79982
|
|
214
215
|
canvas_sdk/utils/__init__.py,sha256=nZEfYeU-qNZBOh39b8zAuEh0Wzh2PgXxs0NRKKe66Pg,184
|
|
215
216
|
canvas_sdk/utils/http.py,sha256=McFtcrgdR2W8XguTaTF54O5Xf5r5buoYDldDM5H5ahY,5846
|
|
216
217
|
canvas_sdk/utils/plugins.py,sha256=853MW2fiLpyG3o9ISEawAthQeRiZP73cai5Tngwu4MY,767
|
|
@@ -219,9 +220,9 @@ canvas_sdk/utils/tests.py,sha256=0Buh_7PvDU1D081_rSJoYSJwIHMOBbL0gtGS3bSKe7s,228
|
|
|
219
220
|
canvas_sdk/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
220
221
|
canvas_sdk/v1/apps.py,sha256=z5HVdICPLYrbWM8ZXK89Xu7RWaXuKU4AFRrPAZGz0C4,151
|
|
221
222
|
canvas_sdk/v1/models.py,sha256=q9Sofiu9JDH5g04H8NHYIqAtBYxH4KjnwOldoKsY9Lk,206
|
|
222
|
-
canvas_sdk/v1/data/__init__.py,sha256=
|
|
223
|
+
canvas_sdk/v1/data/__init__.py,sha256=hCBdaB5IrpIeswI-DehqXb-l72RqeI2aCfmbWskX9TI,3164
|
|
223
224
|
canvas_sdk/v1/data/allergy_intolerance.py,sha256=cm29fCOFiHHHVyff9kreWFUPZM--Nydw3a0FSbWh5-w,2303
|
|
224
|
-
canvas_sdk/v1/data/appointment.py,sha256=
|
|
225
|
+
canvas_sdk/v1/data/appointment.py,sha256=8ApeYiLo4OZHgF5LiTFpJYzwBa96fDyBj6qtKzeAdec,2606
|
|
225
226
|
canvas_sdk/v1/data/assessment.py,sha256=MYUrE6xtOVYSRiMuQKMdHuWFvuGx62w4SZJpyFskQ2U,1485
|
|
226
227
|
canvas_sdk/v1/data/base.py,sha256=_UnCf0SP_HC9FYEzB1BsJ3QcKqXNdCR90Dtx1Gggcf4,6328
|
|
227
228
|
canvas_sdk/v1/data/billing.py,sha256=dPQb9vj2mIhlU5cSfmfTkXw0NRDMmueJojyRNrgx1no,2586
|
|
@@ -243,7 +244,7 @@ canvas_sdk/v1/data/practicelocation.py,sha256=6W2NzwiK0xXlzXfDom_Wm3ScERd8W6mw8o
|
|
|
243
244
|
canvas_sdk/v1/data/protocol_override.py,sha256=o-GK7-HkOsyYDLxUyn9BwlZM-13QNuGGNrey5EF44QI,2105
|
|
244
245
|
canvas_sdk/v1/data/questionnaire.py,sha256=1Vo5QUsOLYbZiQ64QFpgY7Mj6wY8vsV8f9uPiFl0wac,7031
|
|
245
246
|
canvas_sdk/v1/data/reason_for_visit.py,sha256=9rCmvbJZ4dKUUDzqFX1uxlMa_qO57KcRntV3re8q_8g,608
|
|
246
|
-
canvas_sdk/v1/data/staff.py,sha256=
|
|
247
|
+
canvas_sdk/v1/data/staff.py,sha256=HnHM8LJGCJELt562JCcQ8-EECk6CVdP5wHjjS6GfaBY,3477
|
|
247
248
|
canvas_sdk/v1/data/task.py,sha256=5KNn88APPNOHEk4s1ZJRBBav8-AEQTUH039Vio_ZtAk,3471
|
|
248
249
|
canvas_sdk/v1/data/team.py,sha256=FlkZsHSSHKtvmlB1XuGPUmC7wl8iMbOPi_fl9sYw-aU,2801
|
|
249
250
|
canvas_sdk/v1/data/user.py,sha256=XwhYTBuPHWdDc9afaZKB2AA-nHtlT3p7TOvUQo0m20Q,276
|
|
@@ -278,13 +279,12 @@ plugin_runner/aws_headers.py,sha256=DenX_nAMVhXMJZw88PLZbqJsi5_XriNtr3jE-eJqHY4,
|
|
|
278
279
|
plugin_runner/exceptions.py,sha256=YnRZiQVzbU3HrVlmEXLje_np99009YnhTRVHHyBCtqc,433
|
|
279
280
|
plugin_runner/installation.py,sha256=-TntCAveju5vWrKRLnIxy9xn3pnU3goo5dT4tGs-85s,7537
|
|
280
281
|
plugin_runner/plugin_runner.py,sha256=QzMzfRyy-H_SltJCvO0YvqpdX6BVPukA9yPKimla6hI,19632
|
|
281
|
-
plugin_runner/sandbox.py,sha256=
|
|
282
|
+
plugin_runner/sandbox.py,sha256=uoHwLx6Q5tnLwe4cRnQIAFfhN8BKu3g7UWU-IDAtjGY,14543
|
|
282
283
|
plugin_runner/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
283
284
|
plugin_runner/tests/test_application.py,sha256=e1R2YagMRD96gZALx-Zra-e-sR3SiP7cIpI6pheZnUc,2427
|
|
284
285
|
plugin_runner/tests/test_plugin_installer.py,sha256=7sVPfLoAbvfbKJkczyrdhDaOpiqM7EhVJFxSSSvyouo,4422
|
|
285
286
|
plugin_runner/tests/test_plugin_runner.py,sha256=f1DHINHvQTMZS_havJ9Ae0qyRUevGDP-XZA6u8RrG40,13743
|
|
286
287
|
plugin_runner/tests/test_sandbox.py,sha256=fDIkRBzoVWClvIMOI37FvJ1TRZ5mAztt88YZ0OPqsA8,4231
|
|
287
|
-
plugin_runner/tests/data/plugins/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
288
288
|
plugin_runner/tests/fixtures/plugins/example_plugin/CANVAS_MANIFEST.json,sha256=J9T_E5vqUX4HITHbFsd6JQpw3YvMS4wR_lhI5JL2KMk,749
|
|
289
289
|
plugin_runner/tests/fixtures/plugins/example_plugin/README.md,sha256=t9pKwFf8iQPASqdXwfkA5JXkAr8KcSDX6AeW3CMiKVY,246
|
|
290
290
|
plugin_runner/tests/fixtures/plugins/example_plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -355,7 +355,7 @@ protobufs/canvas_generated/messages/plugins.proto,sha256=oNainUPWFYQjgCX7bJEPI9_
|
|
|
355
355
|
protobufs/canvas_generated/services/plugin_runner.proto,sha256=doadBKn5k4xAtOgR-q_pEvW4yzxpUaHNOowMG6CL5GY,304
|
|
356
356
|
pubsub/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
357
357
|
pubsub/pubsub.py,sha256=pyTW0JU8mtaqiAV6g6xjZwel1CVy2EonPMU-_vkmhUM,1044
|
|
358
|
-
canvas-0.
|
|
359
|
-
canvas-0.
|
|
360
|
-
canvas-0.
|
|
361
|
-
canvas-0.
|
|
358
|
+
canvas-0.27.0.dist-info/METADATA,sha256=RKBaiUhbwwjO7UMz3ppAUutlsM42Ep8q9HxT6qEgxhM,4375
|
|
359
|
+
canvas-0.27.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
360
|
+
canvas-0.27.0.dist-info/entry_points.txt,sha256=0Vs_9GmTVUNniH6eDBlRPgofmADMV4BES6Ao26M4AbM,47
|
|
361
|
+
canvas-0.27.0.dist-info/RECORD,,
|
|
@@ -5,10 +5,8 @@ from base64 import b64decode
|
|
|
5
5
|
from collections.abc import Callable
|
|
6
6
|
from functools import cached_property
|
|
7
7
|
from http import HTTPStatus
|
|
8
|
-
from typing import Any, TypeVar
|
|
9
|
-
from urllib.parse import
|
|
10
|
-
|
|
11
|
-
from requests.structures import CaseInsensitiveDict
|
|
8
|
+
from typing import Any, ClassVar, Protocol, TypeVar, cast
|
|
9
|
+
from urllib.parse import parse_qsl
|
|
12
10
|
|
|
13
11
|
from canvas_sdk.effects import Effect, EffectType
|
|
14
12
|
from canvas_sdk.effects.simple_api import JSONResponse, Response
|
|
@@ -19,9 +17,9 @@ from plugin_runner.exceptions import PluginError
|
|
|
19
17
|
|
|
20
18
|
from .exceptions import AuthenticationError, InvalidCredentialsError
|
|
21
19
|
from .security import Credentials
|
|
20
|
+
from .tools import CaseInsensitiveMultiDict, MultiDict, separate_headers
|
|
22
21
|
|
|
23
22
|
# TODO: Routing by path regex?
|
|
24
|
-
# TODO: Support multipart/form-data by adding helpers to the request class
|
|
25
23
|
# TODO: Log requests in a format similar to other API frameworks (probably need effect metadata)
|
|
26
24
|
# TODO: Support Effect metadata that is separate from payload
|
|
27
25
|
# TODO: Encode event payloads with MessagePack instead of JSON
|
|
@@ -30,6 +28,137 @@ from .security import Credentials
|
|
|
30
28
|
JSON = dict[str, "JSON"] | list["JSON"] | int | float | str | bool | None
|
|
31
29
|
|
|
32
30
|
|
|
31
|
+
class FormPart(Protocol):
|
|
32
|
+
"""
|
|
33
|
+
Protocol for representing a form part in the body of a multipart/form-data request.
|
|
34
|
+
|
|
35
|
+
A form part can represent a simple string value, or a file with a content type.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def is_file() -> bool:
|
|
40
|
+
"""Return True or False depending on whether the form part represents a file."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class StringFormPart(FormPart):
|
|
45
|
+
"""Class for representing a form part that is a simple string value."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, name: str, value: str) -> None:
|
|
48
|
+
self.name = name
|
|
49
|
+
self.value = value
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def is_file() -> bool:
|
|
53
|
+
"""Return True or False depending on whether the form part represents a file."""
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
def __eq__(self, other: object) -> bool:
|
|
57
|
+
if isinstance(other, FileFormPart):
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
if not isinstance(other, StringFormPart):
|
|
61
|
+
return NotImplemented
|
|
62
|
+
|
|
63
|
+
return self.name == other.name and self.value == other.value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class FileFormPart(FormPart):
|
|
67
|
+
"""Class for representing a form part that is a file."""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self, name: str, filename: str, content: bytes, content_type: str | None = None
|
|
71
|
+
) -> None:
|
|
72
|
+
self.name = name
|
|
73
|
+
self.filename = filename
|
|
74
|
+
self.content = content
|
|
75
|
+
self.content_type = content_type
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def is_file() -> bool:
|
|
79
|
+
"""Return True or False depending on whether the form part represents a file."""
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def __eq__(self, other: object) -> bool:
|
|
83
|
+
if isinstance(other, StringFormPart):
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
if not isinstance(other, FileFormPart):
|
|
87
|
+
return NotImplemented
|
|
88
|
+
|
|
89
|
+
return all(
|
|
90
|
+
(
|
|
91
|
+
self.name == other.name,
|
|
92
|
+
self.filename == other.filename,
|
|
93
|
+
self.content == other.content,
|
|
94
|
+
self.content_type == other.content_type,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def parse_multipart_form(form: bytes, boundary: str) -> MultiDict[str, FormPart]:
|
|
100
|
+
"""Parse a multipart form and return a dict of string to list of form parts."""
|
|
101
|
+
form_data: list[tuple[str, FormPart]] = []
|
|
102
|
+
|
|
103
|
+
# Split the body by the boundary value and iterate over the parts. The first and last
|
|
104
|
+
# parts can be skipped because there are delimiters on at the start and end of the body
|
|
105
|
+
parts = form.split(f"--{boundary}".encode())
|
|
106
|
+
for part in parts[1:-1]:
|
|
107
|
+
# Each part may be either a simple string value or a file. Simple string values
|
|
108
|
+
# will just have a name and a value, whereas files will have a name, content type,
|
|
109
|
+
# filename, and value.
|
|
110
|
+
name = None
|
|
111
|
+
content_type = None
|
|
112
|
+
filename = None
|
|
113
|
+
|
|
114
|
+
# Split the part into the headers and the value (i.e. the content)
|
|
115
|
+
value: str | bytes
|
|
116
|
+
headers, value = part.split(b"\r\n\r\n", maxsplit=1)
|
|
117
|
+
|
|
118
|
+
# Iterate over the headers and extract the name, filename, and content type
|
|
119
|
+
for header in headers.decode().split("\r\n"):
|
|
120
|
+
# There are only two possible headers: Content-Disposition and Content-Type
|
|
121
|
+
if header.lower().startswith("content-disposition: form-data;"):
|
|
122
|
+
# Iterate over the content disposition parameters to get the form name and
|
|
123
|
+
# filename
|
|
124
|
+
for parameter in header.split(";")[1:]:
|
|
125
|
+
parameter_name, parameter_value = parameter.strip().split("=")
|
|
126
|
+
|
|
127
|
+
# Strip the quotes from the value
|
|
128
|
+
parameter_value = parameter_value[1:-1]
|
|
129
|
+
|
|
130
|
+
if parameter_name == "name":
|
|
131
|
+
name = parameter_value
|
|
132
|
+
elif parameter_name == "filename":
|
|
133
|
+
filename = parameter_value
|
|
134
|
+
elif header.lower().startswith("content-type"):
|
|
135
|
+
# Files will have a content type, so grab it
|
|
136
|
+
content_type = header.split(":")[1].strip()
|
|
137
|
+
|
|
138
|
+
if not name or not value:
|
|
139
|
+
raise RuntimeError("Invalid multipart/form-data request body")
|
|
140
|
+
|
|
141
|
+
# Strip off the trailing newline characters from the value
|
|
142
|
+
value = value[:-2]
|
|
143
|
+
|
|
144
|
+
# Now we have all the data, so append it to the list of form data
|
|
145
|
+
if filename:
|
|
146
|
+
# Because a filename was provided, we know it's a file and not a simple string value
|
|
147
|
+
form_data.append(
|
|
148
|
+
(
|
|
149
|
+
name,
|
|
150
|
+
FileFormPart(
|
|
151
|
+
name=name, filename=filename, content=value, content_type=content_type
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
# Decode the string before adding it
|
|
157
|
+
form_data.append((name, StringFormPart(name, value.decode())))
|
|
158
|
+
|
|
159
|
+
return MultiDict(form_data)
|
|
160
|
+
|
|
161
|
+
|
|
33
162
|
class Request:
|
|
34
163
|
"""Request class for incoming requests to the API."""
|
|
35
164
|
|
|
@@ -38,10 +167,21 @@ class Request:
|
|
|
38
167
|
self.path = event.context["path"]
|
|
39
168
|
self.query_string = event.context["query_string"]
|
|
40
169
|
self._body = event.context["body"]
|
|
41
|
-
self.headers
|
|
42
|
-
|
|
43
|
-
self.query_params =
|
|
44
|
-
|
|
170
|
+
self.headers = CaseInsensitiveMultiDict(separate_headers(event.context["headers"]))
|
|
171
|
+
|
|
172
|
+
self.query_params = MultiDict(parse_qsl(self.query_string))
|
|
173
|
+
|
|
174
|
+
# Parse the content type and any included content type parameters
|
|
175
|
+
content_type = self.headers.get("Content-Type")
|
|
176
|
+
self._content_type_parameters = {}
|
|
177
|
+
if content_type:
|
|
178
|
+
content_type, *parameters = content_type.split(";")
|
|
179
|
+
self.content_type = content_type.strip()
|
|
180
|
+
for parameter in parameters:
|
|
181
|
+
name, value = parameter.strip().split("=")
|
|
182
|
+
self._content_type_parameters[name] = value
|
|
183
|
+
else:
|
|
184
|
+
self.content_type = None
|
|
45
185
|
|
|
46
186
|
@cached_property
|
|
47
187
|
def body(self) -> bytes:
|
|
@@ -49,13 +189,31 @@ class Request:
|
|
|
49
189
|
return b64decode(self._body)
|
|
50
190
|
|
|
51
191
|
def json(self) -> JSON:
|
|
52
|
-
"""Return the response JSON."""
|
|
192
|
+
"""Return the response body as a JSON dict."""
|
|
53
193
|
return json.loads(self.body)
|
|
54
194
|
|
|
55
195
|
def text(self) -> str:
|
|
56
196
|
"""Return the response body as plain text."""
|
|
57
197
|
return self.body.decode()
|
|
58
198
|
|
|
199
|
+
def form_data(self) -> MultiDict[str, FormPart]:
|
|
200
|
+
"""Return the response body as a dict of string to list of FormPart objects."""
|
|
201
|
+
form_data: MultiDict[str, FormPart]
|
|
202
|
+
|
|
203
|
+
if self.content_type == "application/x-www-form-urlencoded":
|
|
204
|
+
# For request bodies that are URL-encoded, just parse them and return them as simple
|
|
205
|
+
# form parts
|
|
206
|
+
form_data = MultiDict(
|
|
207
|
+
(name, StringFormPart(name, value)) for name, value in parse_qsl(self.body.decode())
|
|
208
|
+
)
|
|
209
|
+
elif self.content_type == "multipart/form-data":
|
|
210
|
+
# Parse request bodies that are multipart forms
|
|
211
|
+
form_data = parse_multipart_form(self.body, self._content_type_parameters["boundary"])
|
|
212
|
+
else:
|
|
213
|
+
raise RuntimeError(f"Cannot parse content type {self.content_type} as form data")
|
|
214
|
+
|
|
215
|
+
return form_data
|
|
216
|
+
|
|
59
217
|
|
|
60
218
|
SimpleAPIType = TypeVar("SimpleAPIType", bound="SimpleAPIBase")
|
|
61
219
|
|
|
@@ -108,20 +266,23 @@ class SimpleAPIBase(BaseHandler, ABC):
|
|
|
108
266
|
EventType.Name(EventType.SIMPLE_API_REQUEST),
|
|
109
267
|
]
|
|
110
268
|
|
|
111
|
-
|
|
112
|
-
|
|
269
|
+
_ROUTES: ClassVar[dict[tuple[str, str], RouteHandler]]
|
|
270
|
+
|
|
271
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
272
|
+
super().__init_subclass__(**kwargs)
|
|
113
273
|
|
|
114
274
|
# Build the registry of routes so that requests can be routed to the correct handler. This
|
|
115
|
-
# is done by iterating over the methods on the class
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
for attr in
|
|
275
|
+
# is done by iterating over the methods on the class and looking for methods that have been
|
|
276
|
+
# marked by the handler decorators (get, post, etc.).
|
|
277
|
+
cls._ROUTES = {}
|
|
278
|
+
for attr in cls.__dict__.values():
|
|
119
279
|
if callable(attr) and (route := getattr(attr, "route", None)):
|
|
120
280
|
method, relative_path = route
|
|
121
|
-
path = f"{
|
|
122
|
-
|
|
281
|
+
path = f"{cls._path_prefix()}{relative_path}"
|
|
282
|
+
cls._ROUTES[(method, path)] = attr
|
|
123
283
|
|
|
124
|
-
|
|
284
|
+
@classmethod
|
|
285
|
+
def _path_prefix(cls) -> str:
|
|
125
286
|
return ""
|
|
126
287
|
|
|
127
288
|
@cached_property
|
|
@@ -174,7 +335,7 @@ class SimpleAPIBase(BaseHandler, ABC):
|
|
|
174
335
|
def _handle_request(self) -> list[Effect]:
|
|
175
336
|
"""Route the incoming request to the handler method based on the HTTP method and path."""
|
|
176
337
|
# Get the handler method
|
|
177
|
-
handler = self.
|
|
338
|
+
handler = self._ROUTES[(self.request.method, self.request.path)]
|
|
178
339
|
|
|
179
340
|
# Handle the request
|
|
180
341
|
effects = handler(self)
|
|
@@ -191,7 +352,7 @@ class SimpleAPIBase(BaseHandler, ABC):
|
|
|
191
352
|
if isinstance(effect, Response):
|
|
192
353
|
effects[index] = effect.apply()
|
|
193
354
|
|
|
194
|
-
if effects[index].type == EffectType.SIMPLE_API_RESPONSE:
|
|
355
|
+
if cast(Effect, effects[index]).type == EffectType.SIMPLE_API_RESPONSE:
|
|
195
356
|
# If a response has already been found, return an error response immediately
|
|
196
357
|
if response_found:
|
|
197
358
|
log.error(f"Multiple responses provided by {SimpleAPI.__name__} handler")
|
|
@@ -210,13 +371,13 @@ class SimpleAPIBase(BaseHandler, ABC):
|
|
|
210
371
|
# If the handler returned an error response, return only that response effect
|
|
211
372
|
# and omit any other included effects
|
|
212
373
|
if 400 <= status_code <= 599:
|
|
213
|
-
return [effects[index]]
|
|
374
|
+
return [cast(Effect, effects[index])]
|
|
214
375
|
|
|
215
|
-
return effects
|
|
376
|
+
return cast(list[Effect], effects)
|
|
216
377
|
|
|
217
378
|
def accept_event(self) -> bool:
|
|
218
379
|
"""Ignore the event if the handler does not implement the route."""
|
|
219
|
-
return (self.request.method, self.request.path) in self.
|
|
380
|
+
return (self.request.method, self.request.path) in self._ROUTES
|
|
220
381
|
|
|
221
382
|
def authenticate(self, credentials: Credentials) -> bool:
|
|
222
383
|
"""Method the user should override to authenticate requests."""
|
|
@@ -257,8 +418,12 @@ class SimpleAPI(SimpleAPIBase, ABC):
|
|
|
257
418
|
f"class attributes: {', '.join(f'{cls.__name__}.{name}' for name in names)}"
|
|
258
419
|
)
|
|
259
420
|
|
|
260
|
-
|
|
261
|
-
|
|
421
|
+
@classmethod
|
|
422
|
+
def _path_prefix(cls) -> str:
|
|
423
|
+
# getattr needs a default else it will raise an exception. We also need to ensure that the
|
|
424
|
+
# final value is a string if the user specifies "None" as the prefix because this value gets
|
|
425
|
+
# prepended to the URL path
|
|
426
|
+
return getattr(cls, "PREFIX", "") or ""
|
|
262
427
|
|
|
263
428
|
|
|
264
429
|
class SimpleAPIRoute(SimpleAPIBase, ABC):
|
|
@@ -271,8 +436,6 @@ class SimpleAPIRoute(SimpleAPIBase, ABC):
|
|
|
271
436
|
f"Setting a PREFIX value on a {SimpleAPIRoute.__name__} is not allowed"
|
|
272
437
|
)
|
|
273
438
|
|
|
274
|
-
super().__init_subclass__(**kwargs)
|
|
275
|
-
|
|
276
439
|
for attr_name, attr_value in cls.__dict__.items():
|
|
277
440
|
decorator: Callable | None
|
|
278
441
|
match attr_name:
|
|
@@ -307,6 +470,8 @@ class SimpleAPIRoute(SimpleAPIBase, ABC):
|
|
|
307
470
|
|
|
308
471
|
decorator(path)(attr_value)
|
|
309
472
|
|
|
473
|
+
super().__init_subclass__(**kwargs)
|
|
474
|
+
|
|
310
475
|
def get(self) -> list[Response | Effect]:
|
|
311
476
|
"""Stub method for GET handler."""
|
|
312
477
|
return []
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from collections.abc import ItemsView, Iterable, Iterator, KeysView, Mapping, ValuesView
|
|
2
|
+
from typing import Any, TypeVar, overload
|
|
3
|
+
|
|
4
|
+
KeyType = TypeVar("KeyType")
|
|
5
|
+
ValueType = TypeVar("ValueType", covariant=True)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MultiDict(Mapping[KeyType, ValueType]):
|
|
9
|
+
"""Immutable key-value data structure that can store multiple values per key."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, items: Iterable[tuple[KeyType, ValueType]] | None = None) -> None:
|
|
12
|
+
self._dict: dict[KeyType, ValueType] = {}
|
|
13
|
+
self._items = []
|
|
14
|
+
for key, value in items or ():
|
|
15
|
+
if key not in self._dict:
|
|
16
|
+
self._dict[key] = value
|
|
17
|
+
self._items.append((key, value))
|
|
18
|
+
|
|
19
|
+
def __len__(self) -> int:
|
|
20
|
+
return len(self._dict)
|
|
21
|
+
|
|
22
|
+
def __iter__(self) -> Iterator[KeyType]:
|
|
23
|
+
return iter(self.keys())
|
|
24
|
+
|
|
25
|
+
def __getitem__(self, key: KeyType, /) -> ValueType:
|
|
26
|
+
return self._dict[key]
|
|
27
|
+
|
|
28
|
+
def __contains__(self, x: object, /) -> bool:
|
|
29
|
+
return x in self._dict
|
|
30
|
+
|
|
31
|
+
@overload
|
|
32
|
+
def get(self, __key: KeyType, /) -> Any: ...
|
|
33
|
+
|
|
34
|
+
@overload
|
|
35
|
+
def get(self, __key: KeyType, /, __default: Any = None) -> Any: ...
|
|
36
|
+
|
|
37
|
+
def get(self, __key: KeyType, __default: Any | None = None) -> Any:
|
|
38
|
+
"""Get a value for a key if present, and if not, return the default value."""
|
|
39
|
+
return self._dict.get(__key, __default)
|
|
40
|
+
|
|
41
|
+
def get_list(self, __key: KeyType) -> list[ValueType]:
|
|
42
|
+
"""Get the values for a key if present, and if not, return an empty list."""
|
|
43
|
+
return [value for key, value in self._items if key == __key]
|
|
44
|
+
|
|
45
|
+
def items(self) -> ItemsView[KeyType, ValueType]:
|
|
46
|
+
"""Return an items view of the dict."""
|
|
47
|
+
return self._dict.items()
|
|
48
|
+
|
|
49
|
+
def multi_items(self) -> Iterable[tuple[KeyType, ValueType]]:
|
|
50
|
+
"""Return an iterable of tuples of the keys and values in the dict."""
|
|
51
|
+
yield from self._items
|
|
52
|
+
|
|
53
|
+
def keys(self) -> KeysView[KeyType]:
|
|
54
|
+
"""Return a keys view of the dict."""
|
|
55
|
+
return self._dict.keys()
|
|
56
|
+
|
|
57
|
+
def __reversed__(self) -> Iterator[KeyType]:
|
|
58
|
+
return iter(reversed(list(self.keys())))
|
|
59
|
+
|
|
60
|
+
def values(self) -> ValuesView[ValueType]:
|
|
61
|
+
"""Return a values view of the dict."""
|
|
62
|
+
return self._dict.values()
|
|
63
|
+
|
|
64
|
+
def __eq__(self, other: object) -> bool:
|
|
65
|
+
if not isinstance(other, MultiDict):
|
|
66
|
+
return NotImplemented
|
|
67
|
+
|
|
68
|
+
return other._items == self._items
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class CaseInsensitiveMultiDict(MultiDict[str, ValueType]):
|
|
72
|
+
"""
|
|
73
|
+
Case-insensitive immutable key-value data structure that can store multiple values per key.
|
|
74
|
+
|
|
75
|
+
Keys in the dict are interpreted in a case-insensitive manner.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, items: Iterable[tuple[str, ValueType]] | None = None) -> None:
|
|
79
|
+
super().__init__(((key.lower(), value) for key, value in items or ()))
|
|
80
|
+
|
|
81
|
+
def __getitem__(self, key: str, /) -> ValueType:
|
|
82
|
+
return super().__getitem__(key.lower())
|
|
83
|
+
|
|
84
|
+
def __contains__(self, x: object, /) -> bool:
|
|
85
|
+
if not isinstance(x, str):
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
return super().__contains__(x.lower())
|
|
89
|
+
|
|
90
|
+
def get(self, __key: KeyType, __default: Any | None = None) -> Any:
|
|
91
|
+
"""Get a value for a key if present, and if not, return the default value."""
|
|
92
|
+
if not isinstance(__key, str):
|
|
93
|
+
return __default
|
|
94
|
+
|
|
95
|
+
return super().get(__key.lower(), __default)
|
|
96
|
+
|
|
97
|
+
def get_list(self, __key: KeyType) -> list[ValueType]:
|
|
98
|
+
"""Get the values for a key if present, and if not, return an empty list."""
|
|
99
|
+
if not isinstance(__key, str):
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
return super().get_list(__key.lower())
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def separate_headers(headers: Mapping[str, str]) -> list[tuple[str, str]]:
|
|
106
|
+
"""
|
|
107
|
+
Break apart header values containing comma-separated lists into discrete key-value pairs.
|
|
108
|
+
"""
|
|
109
|
+
headers_list = []
|
|
110
|
+
|
|
111
|
+
for key, values in headers.items():
|
|
112
|
+
for value in values.split(","):
|
|
113
|
+
headers_list.append((key, value.strip()))
|
|
114
|
+
|
|
115
|
+
return headers_list
|
|
@@ -3,8 +3,7 @@ from base64 import b64decode, b64encode
|
|
|
3
3
|
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
4
4
|
from http import HTTPStatus
|
|
5
5
|
from types import SimpleNamespace
|
|
6
|
-
from typing import Any
|
|
7
|
-
from urllib.parse import parse_qs
|
|
6
|
+
from typing import Any, TypeVar
|
|
8
7
|
from uuid import uuid4
|
|
9
8
|
|
|
10
9
|
import pytest
|
|
@@ -20,7 +19,15 @@ from canvas_sdk.effects.simple_api import (
|
|
|
20
19
|
)
|
|
21
20
|
from canvas_sdk.events import Event, EventRequest, EventType
|
|
22
21
|
from canvas_sdk.handlers.simple_api import api
|
|
23
|
-
from canvas_sdk.handlers.simple_api.api import
|
|
22
|
+
from canvas_sdk.handlers.simple_api.api import (
|
|
23
|
+
FileFormPart,
|
|
24
|
+
FormPart,
|
|
25
|
+
Request,
|
|
26
|
+
SimpleAPI,
|
|
27
|
+
SimpleAPIBase,
|
|
28
|
+
SimpleAPIRoute,
|
|
29
|
+
StringFormPart,
|
|
30
|
+
)
|
|
24
31
|
from canvas_sdk.handlers.simple_api.security import (
|
|
25
32
|
APIKeyAuthMixin,
|
|
26
33
|
APIKeyCredentials,
|
|
@@ -30,10 +37,26 @@ from canvas_sdk.handlers.simple_api.security import (
|
|
|
30
37
|
BearerCredentials,
|
|
31
38
|
Credentials,
|
|
32
39
|
)
|
|
40
|
+
from canvas_sdk.handlers.simple_api.tools import (
|
|
41
|
+
CaseInsensitiveMultiDict,
|
|
42
|
+
MultiDict,
|
|
43
|
+
separate_headers,
|
|
44
|
+
)
|
|
33
45
|
from plugin_runner.exceptions import PluginError
|
|
34
46
|
|
|
35
47
|
REQUEST_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
|
36
|
-
|
|
48
|
+
HEADERS_RAW = {
|
|
49
|
+
"Canvas-Plugins-Test-Header-1": "test header 1",
|
|
50
|
+
"Canvas-Plugins-Test-Header-2": "test header 2a, test header 2b",
|
|
51
|
+
}
|
|
52
|
+
HEADERS = CaseInsensitiveMultiDict(separate_headers(HEADERS_RAW))
|
|
53
|
+
|
|
54
|
+
FORM = b64decode(
|
|
55
|
+
"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"
|
|
56
|
+
)
|
|
57
|
+
FILE = b64decode(
|
|
58
|
+
"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"
|
|
59
|
+
)
|
|
37
60
|
|
|
38
61
|
|
|
39
62
|
class NoAuthMixin:
|
|
@@ -78,7 +101,7 @@ def make_event(
|
|
|
78
101
|
"path": path,
|
|
79
102
|
"query_string": query_string or "",
|
|
80
103
|
"body": b64encode(body or b"").decode(),
|
|
81
|
-
"headers": headers
|
|
104
|
+
"headers": dict(headers) if headers else {},
|
|
82
105
|
},
|
|
83
106
|
indent=None,
|
|
84
107
|
separators=(",", ":"),
|
|
@@ -117,36 +140,107 @@ def handle_request(
|
|
|
117
140
|
return handler.compute()
|
|
118
141
|
|
|
119
142
|
|
|
143
|
+
T = TypeVar("T")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@pytest.mark.parametrize(
|
|
147
|
+
argnames="func,expected_value",
|
|
148
|
+
argvalues=[
|
|
149
|
+
(lambda m: m["b"], 2),
|
|
150
|
+
(lambda m: m["a"], 1),
|
|
151
|
+
(lambda m: len(m), 2),
|
|
152
|
+
(lambda m: next(iter(m.items())), ("a", 1)),
|
|
153
|
+
(lambda m: "a" in m, True),
|
|
154
|
+
(lambda m: "d" in m, False),
|
|
155
|
+
(lambda m: m.get("a"), 1),
|
|
156
|
+
(lambda m: m.get("d", 4), 4),
|
|
157
|
+
(lambda m: m.get("d"), None),
|
|
158
|
+
(lambda m: m.get_list("a"), [1, 3]),
|
|
159
|
+
(
|
|
160
|
+
lambda m: [(k, v) for k, v in m.items()],
|
|
161
|
+
[("a", 1), ("b", 2)],
|
|
162
|
+
),
|
|
163
|
+
(
|
|
164
|
+
lambda m: [(k, v) for k, v in m.multi_items()],
|
|
165
|
+
[("a", 1), ("b", 2), ("a", 3)],
|
|
166
|
+
),
|
|
167
|
+
(lambda m: list(m.keys()), ["a", "b"]),
|
|
168
|
+
(lambda m: list(reversed(m)), ["b", "a"]),
|
|
169
|
+
(lambda m: list(m.values()), [1, 2]),
|
|
170
|
+
(lambda m: m == MultiDict((("a", 1), ("b", 2), ("a", 3))), True),
|
|
171
|
+
(lambda m: m != MultiDict((("a", 1), ("b", 2))), True),
|
|
172
|
+
],
|
|
173
|
+
ids=[
|
|
174
|
+
"[] single value from single value",
|
|
175
|
+
"[] single value from multiple values",
|
|
176
|
+
"len",
|
|
177
|
+
"iter",
|
|
178
|
+
"in",
|
|
179
|
+
"not in",
|
|
180
|
+
"get",
|
|
181
|
+
"get default",
|
|
182
|
+
"get no default",
|
|
183
|
+
"get_list",
|
|
184
|
+
"items",
|
|
185
|
+
"multi_items",
|
|
186
|
+
"keys",
|
|
187
|
+
"reversed",
|
|
188
|
+
"values",
|
|
189
|
+
"==",
|
|
190
|
+
"!=",
|
|
191
|
+
],
|
|
192
|
+
)
|
|
193
|
+
def test_multidict(func: Callable[[MultiDict[str, int]], T], expected_value: T) -> None:
|
|
194
|
+
"""Test the methods and functionality of MultiDict."""
|
|
195
|
+
multidict = MultiDict((("a", 1), ("b", 2), ("a", 3)))
|
|
196
|
+
assert func(multidict) == expected_value
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pytest.mark.parametrize(
|
|
200
|
+
argnames="func,expected_value",
|
|
201
|
+
argvalues=[
|
|
202
|
+
(lambda m: m["b"] == m["B"] == 2, True),
|
|
203
|
+
(lambda m: "a" in m and "A" in m, True),
|
|
204
|
+
(lambda m: "d" not in m and "D" not in m, True),
|
|
205
|
+
(lambda m: m.get("a") == m.get("A") == 1, True),
|
|
206
|
+
(lambda m: m.get_list("a") == m.get_list("A") == [1, 3], True),
|
|
207
|
+
],
|
|
208
|
+
ids=["[]", "in", "not in", "get", "get_list"],
|
|
209
|
+
)
|
|
210
|
+
def test_case_insensitive_multidict(
|
|
211
|
+
func: Callable[[MultiDict[str, int]], T], expected_value: T
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Test the methods and functionality of CaseInsensitiveMultiDict."""
|
|
214
|
+
multidict = CaseInsensitiveMultiDict((("a", 1), ("b", 2), ("A", 3)))
|
|
215
|
+
assert func(multidict) == expected_value
|
|
216
|
+
|
|
217
|
+
|
|
120
218
|
@pytest.mark.parametrize(
|
|
121
|
-
argnames="method,
|
|
219
|
+
argnames="method,body,headers",
|
|
122
220
|
argvalues=[
|
|
123
|
-
("GET",
|
|
221
|
+
("GET", b"", HEADERS_RAW),
|
|
124
222
|
(
|
|
125
223
|
"POST",
|
|
126
|
-
"/route",
|
|
127
|
-
"value1=a&value2=b",
|
|
128
224
|
b'{"message": "JSON request"}',
|
|
129
|
-
{"Content-Type": "application/json"},
|
|
225
|
+
{"Content-Type": "application/json"} | HEADERS_RAW,
|
|
130
226
|
),
|
|
131
227
|
(
|
|
132
228
|
"POST",
|
|
133
|
-
"/route",
|
|
134
|
-
"value1=a&value2=b",
|
|
135
229
|
b"plain text request",
|
|
136
|
-
{"Content-Type": "text/plain"},
|
|
230
|
+
{"Content-Type": "text/plain"} | HEADERS_RAW,
|
|
137
231
|
),
|
|
138
|
-
("POST",
|
|
232
|
+
("POST", b"<html></html>", {"Content-Type": "text/html"} | HEADERS_RAW),
|
|
139
233
|
],
|
|
140
234
|
ids=["no body", "JSON", "plain text", "HTML"],
|
|
141
235
|
)
|
|
142
236
|
def test_request(
|
|
143
237
|
method: str,
|
|
144
|
-
path: str,
|
|
145
|
-
query_string: str | None,
|
|
146
238
|
body: bytes,
|
|
147
|
-
headers: Mapping[str, str]
|
|
239
|
+
headers: Mapping[str, str],
|
|
148
240
|
) -> None:
|
|
149
241
|
"""Test the construction of a Request object and access to its attributes."""
|
|
242
|
+
path = "/route"
|
|
243
|
+
query_string = "value1=a&value2=b"
|
|
150
244
|
request = Request(
|
|
151
245
|
make_event(EventType.SIMPLE_API_REQUEST, method, path, query_string, body, headers)
|
|
152
246
|
)
|
|
@@ -155,11 +249,19 @@ def test_request(
|
|
|
155
249
|
assert request.path == path
|
|
156
250
|
assert request.query_string == query_string
|
|
157
251
|
assert request.body == body
|
|
158
|
-
assert request.headers == headers
|
|
159
252
|
|
|
160
|
-
assert request.
|
|
161
|
-
assert request.
|
|
162
|
-
assert request.
|
|
253
|
+
assert request.headers == CaseInsensitiveMultiDict(separate_headers(headers))
|
|
254
|
+
assert request.headers["canvas-plugins-test-header-1"] == "test header 1"
|
|
255
|
+
assert request.headers["canvas-plugins-test-header-2"] == "test header 2a"
|
|
256
|
+
assert request.headers.get_list("canvas-plugins-test-header-1") == ["test header 1"]
|
|
257
|
+
assert request.headers.get_list("canvas-plugins-test-header-2") == [
|
|
258
|
+
"test header 2a",
|
|
259
|
+
"test header 2b",
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
assert request.query_params == MultiDict((("value1", "a"), ("value2", "b")))
|
|
263
|
+
|
|
264
|
+
assert request.content_type == request.headers.get("content-type")
|
|
163
265
|
|
|
164
266
|
if request.content_type:
|
|
165
267
|
if request.content_type == "application/json":
|
|
@@ -168,6 +270,59 @@ def test_request(
|
|
|
168
270
|
assert request.text() == body.decode()
|
|
169
271
|
|
|
170
272
|
|
|
273
|
+
@pytest.mark.parametrize(
|
|
274
|
+
argnames="body,content_type,expected_form_data",
|
|
275
|
+
argvalues=[
|
|
276
|
+
(
|
|
277
|
+
b"part1=value1&part2=value2&part1=value3",
|
|
278
|
+
"application/x-www-form-urlencoded",
|
|
279
|
+
MultiDict(
|
|
280
|
+
(
|
|
281
|
+
("part1", StringFormPart(name="part1", value="value1")),
|
|
282
|
+
("part2", StringFormPart(name="part2", value="value2")),
|
|
283
|
+
("part1", StringFormPart(name="part1", value="value3")),
|
|
284
|
+
)
|
|
285
|
+
),
|
|
286
|
+
),
|
|
287
|
+
(
|
|
288
|
+
FORM,
|
|
289
|
+
"multipart/form-data; boundary=--------------------------966149001464621638881292",
|
|
290
|
+
MultiDict(
|
|
291
|
+
(
|
|
292
|
+
("part1", StringFormPart(name="part1", value="value1")),
|
|
293
|
+
("part2", StringFormPart(name="part2", value="value2")),
|
|
294
|
+
(
|
|
295
|
+
"part1",
|
|
296
|
+
FileFormPart(
|
|
297
|
+
name="part1",
|
|
298
|
+
filename="Sydney.jpg",
|
|
299
|
+
content=FILE,
|
|
300
|
+
content_type="image/jpeg",
|
|
301
|
+
),
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
),
|
|
305
|
+
),
|
|
306
|
+
],
|
|
307
|
+
ids=["x-www-form-urlencoded", "multipart/form-data"],
|
|
308
|
+
)
|
|
309
|
+
def test_request_form(
|
|
310
|
+
body: bytes, content_type: str, expected_form_data: Mapping[str, Sequence[FormPart]]
|
|
311
|
+
) -> None:
|
|
312
|
+
"""Test the parsing of form data from the request body."""
|
|
313
|
+
request = Request(
|
|
314
|
+
make_event(
|
|
315
|
+
EventType.SIMPLE_API_REQUEST,
|
|
316
|
+
method="POST",
|
|
317
|
+
path="/route",
|
|
318
|
+
body=body,
|
|
319
|
+
headers={"Content-Type": content_type},
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
assert request.form_data() == expected_form_data
|
|
324
|
+
|
|
325
|
+
|
|
171
326
|
def response_body(effects: Iterable[Effect]) -> bytes:
|
|
172
327
|
"""Given a list of effects, find the response object and return the body."""
|
|
173
328
|
for effect in effects:
|
|
@@ -311,7 +466,7 @@ def test_request_lifecycle() -> None:
|
|
|
311
466
|
|
|
312
467
|
assert body == {
|
|
313
468
|
"body": {"message": "JSON request"},
|
|
314
|
-
"headers": HEADERS,
|
|
469
|
+
"headers": {k.lower(): v for k, v in HEADERS.items()},
|
|
315
470
|
"method": "POST",
|
|
316
471
|
"path": "/route",
|
|
317
472
|
"query_string": "value1=a&value2=b",
|
|
@@ -448,8 +603,9 @@ def test_response(response: Callable, expected_effects: Sequence[Effect]) -> Non
|
|
|
448
603
|
headers=HEADERS,
|
|
449
604
|
content_type="application/pdf",
|
|
450
605
|
),
|
|
451
|
-
'{"headers": {"
|
|
452
|
-
'"
|
|
606
|
+
'{"headers": {"canvas-plugins-test-header-1": "test header 1", '
|
|
607
|
+
'"canvas-plugins-test-header-2": "test header 2a", "Content-Type": "application/pdf"}, '
|
|
608
|
+
'"body": "JVBERi0xLjQKJdPr6eE=", "status_code": 202}',
|
|
453
609
|
),
|
|
454
610
|
(
|
|
455
611
|
JSONResponse(
|
|
@@ -457,26 +613,28 @@ def test_response(response: Callable, expected_effects: Sequence[Effect]) -> Non
|
|
|
457
613
|
status_code=HTTPStatus.ACCEPTED,
|
|
458
614
|
headers=HEADERS,
|
|
459
615
|
),
|
|
460
|
-
'{"headers": {"
|
|
461
|
-
'"
|
|
462
|
-
'"status_code": 202}',
|
|
616
|
+
'{"headers": {"canvas-plugins-test-header-1": "test header 1", '
|
|
617
|
+
'"canvas-plugins-test-header-2": "test header 2a", "Content-Type": "application/json"},'
|
|
618
|
+
' "body": "eyJtZXNzYWdlIjogIkpTT04gcmVzcG9uc2UifQ==", "status_code": 202}',
|
|
463
619
|
),
|
|
464
620
|
(
|
|
465
621
|
PlainTextResponse(
|
|
466
622
|
content="plain text response", status_code=HTTPStatus.ACCEPTED, headers=HEADERS
|
|
467
623
|
),
|
|
468
|
-
'{"headers": {"
|
|
469
|
-
'"
|
|
624
|
+
'{"headers": {"canvas-plugins-test-header-1": "test header 1", '
|
|
625
|
+
'"canvas-plugins-test-header-2": "test header 2a", "Content-Type": "text/plain"}, '
|
|
626
|
+
'"body": "cGxhaW4gdGV4dCByZXNwb25zZQ==", "status_code": 202}',
|
|
470
627
|
),
|
|
471
628
|
(
|
|
472
629
|
HTMLResponse(content="<html></html>", status_code=HTTPStatus.ACCEPTED, headers=HEADERS),
|
|
473
|
-
'{"headers": {"
|
|
474
|
-
'"
|
|
630
|
+
'{"headers": {"canvas-plugins-test-header-1": "test header 1", '
|
|
631
|
+
'"canvas-plugins-test-header-2": "test header 2a", "Content-Type": "text/html"}, '
|
|
632
|
+
'"body": "PGh0bWw+PC9odG1sPg==", "status_code": 202}',
|
|
475
633
|
),
|
|
476
634
|
(
|
|
477
635
|
Response(status_code=HTTPStatus.NO_CONTENT, headers=HEADERS),
|
|
478
|
-
'{"headers": {"
|
|
479
|
-
'"status_code": 204}',
|
|
636
|
+
'{"headers": {"canvas-plugins-test-header-1": "test header 1", '
|
|
637
|
+
'"canvas-plugins-test-header-2": "test header 2a"}, "body": "", "status_code": 204}',
|
|
480
638
|
),
|
|
481
639
|
],
|
|
482
640
|
ids=["binary", "JSON", "plain text", "HTML", "no content"],
|
canvas_sdk/v1/data/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .allergy_intolerance import AllergyIntolerance, AllergyIntoleranceCoding
|
|
2
|
-
from .appointment import Appointment
|
|
2
|
+
from .appointment import Appointment, AppointmentExternalIdentifier
|
|
3
3
|
from .assessment import Assessment
|
|
4
4
|
from .billing import BillingLineItem, BillingLineItemModifier
|
|
5
5
|
from .care_team import CareTeamMembership, CareTeamRole
|
|
@@ -49,12 +49,13 @@ from .questionnaire import (
|
|
|
49
49
|
ResponseOptionSet,
|
|
50
50
|
)
|
|
51
51
|
from .reason_for_visit import ReasonForVisitSettingCoding
|
|
52
|
-
from .staff import Staff
|
|
52
|
+
from .staff import Staff, StaffContactPoint
|
|
53
53
|
from .task import Task, TaskComment, TaskLabel, TaskTaskLabel
|
|
54
54
|
from .user import CanvasUser
|
|
55
55
|
|
|
56
56
|
__all__ = [
|
|
57
57
|
"Appointment",
|
|
58
|
+
"AppointmentExternalIdentifier",
|
|
58
59
|
"AllergyIntolerance",
|
|
59
60
|
"AllergyIntoleranceCoding",
|
|
60
61
|
"Assessment",
|
|
@@ -109,6 +110,7 @@ __all__ = [
|
|
|
109
110
|
"ResponseOption",
|
|
110
111
|
"ResponseOptionSet",
|
|
111
112
|
"Staff",
|
|
113
|
+
"StaffContactPoint",
|
|
112
114
|
"Task",
|
|
113
115
|
"TaskComment",
|
|
114
116
|
"TaskLabel",
|
|
@@ -54,3 +54,25 @@ class Appointment(models.Model):
|
|
|
54
54
|
telehealth_instructions_sent = models.BooleanField()
|
|
55
55
|
location = models.ForeignKey("v1.PracticeLocation", on_delete=models.DO_NOTHING, null=True)
|
|
56
56
|
description = models.TextField(null=True, blank=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AppointmentExternalIdentifier(models.Model):
|
|
60
|
+
"""AppointmentExternalIdentifier."""
|
|
61
|
+
|
|
62
|
+
class Meta:
|
|
63
|
+
managed = False
|
|
64
|
+
db_table = "canvas_sdk_data_api_appointmentexternalidentifier_001"
|
|
65
|
+
|
|
66
|
+
id = models.UUIDField()
|
|
67
|
+
dbid = models.BigIntegerField(primary_key=True)
|
|
68
|
+
created = models.DateTimeField()
|
|
69
|
+
modified = models.DateTimeField()
|
|
70
|
+
use = models.CharField()
|
|
71
|
+
identifier_type = models.CharField()
|
|
72
|
+
system = models.CharField()
|
|
73
|
+
value = models.CharField()
|
|
74
|
+
issued_date = models.DateField()
|
|
75
|
+
expiration_date = models.DateField()
|
|
76
|
+
appointment = models.ForeignKey(
|
|
77
|
+
Appointment, on_delete=models.DO_NOTHING, related_name="external_identifiers"
|
|
78
|
+
)
|
canvas_sdk/v1/data/staff.py
CHANGED
|
@@ -2,7 +2,13 @@ from django.contrib.postgres.fields import ArrayField
|
|
|
2
2
|
from django.db import models
|
|
3
3
|
from timezone_utils.fields import TimeZoneField
|
|
4
4
|
|
|
5
|
-
from canvas_sdk.v1.data.common import
|
|
5
|
+
from canvas_sdk.v1.data.common import (
|
|
6
|
+
ContactPointState,
|
|
7
|
+
ContactPointSystem,
|
|
8
|
+
ContactPointUse,
|
|
9
|
+
PersonSex,
|
|
10
|
+
TaxIDType,
|
|
11
|
+
)
|
|
6
12
|
|
|
7
13
|
|
|
8
14
|
class Staff(models.Model):
|
|
@@ -61,3 +67,21 @@ class Staff(models.Model):
|
|
|
61
67
|
default_supervising_provider = models.ForeignKey(
|
|
62
68
|
"v1.Staff", on_delete=models.DO_NOTHING, related_name="supervising_team", null=True
|
|
63
69
|
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class StaffContactPoint(models.Model):
|
|
73
|
+
"""StaffContactPoint."""
|
|
74
|
+
|
|
75
|
+
class Meta:
|
|
76
|
+
managed = False
|
|
77
|
+
db_table = "canvas_sdk_data_api_staffcontactpoint_001"
|
|
78
|
+
|
|
79
|
+
id = models.UUIDField()
|
|
80
|
+
dbid = models.BigIntegerField(primary_key=True)
|
|
81
|
+
system = models.CharField(choices=ContactPointSystem.choices)
|
|
82
|
+
value = models.CharField()
|
|
83
|
+
use = models.CharField(choices=ContactPointUse.choices)
|
|
84
|
+
use_notes = models.CharField()
|
|
85
|
+
rank = models.IntegerField()
|
|
86
|
+
state = models.CharField(choices=ContactPointState.choices)
|
|
87
|
+
staff = models.ForeignKey(Staff, on_delete=models.DO_NOTHING, related_name="telecom")
|
plugin_runner/sandbox.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|