canvas 0.57.0__py3-none-any.whl → 0.58.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.57.0.dist-info → canvas-0.58.0.dist-info}/METADATA +4 -1
- {canvas-0.57.0.dist-info → canvas-0.58.0.dist-info}/RECORD +20 -15
- canvas_cli/apps/run_plugins/run_plugins.py +90 -4
- canvas_cli/templates/plugins/default/cookiecutter.json +2 -1
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/pyproject.toml +16 -0
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/{CANVAS_MANIFEST.json → {{ cookiecutter.__package_name }}/CANVAS_MANIFEST.json } +2 -2
- canvas_sdk/test_utils/__init__.py +0 -0
- canvas_sdk/test_utils/factories/__init__.py +4 -0
- canvas_sdk/test_utils/factories/patient.py +39 -0
- canvas_sdk/test_utils/factories/user.py +13 -0
- canvas_sdk/v1/data/patient.py +67 -47
- canvas_sdk/v1/data/user.py +2 -2
- canvas_sdk/v1/data/utils.py +16 -0
- plugin_runner/plugin_runner.py +8 -1
- settings.py +9 -6
- {canvas-0.57.0.dist-info → canvas-0.58.0.dist-info}/WHEEL +0 -0
- {canvas-0.57.0.dist-info → canvas-0.58.0.dist-info}/entry_points.txt +0 -0
- /canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/{README.md → {{ cookiecutter.__package_name }}/README.md} +0 -0
- /canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/{protocols → {{ cookiecutter.__package_name }}/protocols}/__init__.py +0 -0
- /canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/{protocols → {{ cookiecutter.__package_name }}/protocols}/my_protocol.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: canvas
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.58.0
|
|
4
4
|
Summary: SDK to customize event-driven actions in your Canvas instance
|
|
5
5
|
Author-email: Canvas Team <engineering@canvasmedical.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,6 +13,7 @@ Requires-Dist: django-stubs[compatible-mypy]<6,>=5.1.1
|
|
|
13
13
|
Requires-Dist: django-timezone-utils<0.16,>=0.15.0
|
|
14
14
|
Requires-Dist: django<6,>=5.1.1
|
|
15
15
|
Requires-Dist: env-tools<3,>=2.4.0
|
|
16
|
+
Requires-Dist: factory-boy>=3.3.3
|
|
16
17
|
Requires-Dist: frozendict>=2.4.6
|
|
17
18
|
Requires-Dist: grpcio<2,>=1.60.1
|
|
18
19
|
Requires-Dist: ipython<9,>=8.21.0
|
|
@@ -32,6 +33,8 @@ Requires-Dist: statsd<5,>=4.0.1
|
|
|
32
33
|
Requires-Dist: typer
|
|
33
34
|
Requires-Dist: typing-extensions<4.13,>=4.8
|
|
34
35
|
Requires-Dist: websocket-client<2,>=1.7.0
|
|
36
|
+
Provides-Extra: test-utils
|
|
37
|
+
Requires-Dist: pytest-canvas; extra == 'test-utils'
|
|
35
38
|
Description-Content-Type: text/markdown
|
|
36
39
|
|
|
37
40
|
[](https://codecov.io/gh/canvas-medical/canvas-plugins)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
settings.py,sha256=
|
|
1
|
+
settings.py,sha256=8CGtEpfAZ43FM6XPNJ8CgGwhWU9xn5mmncD7HZWi-Vk,6321
|
|
2
2
|
canvas_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
canvas_cli/main.py,sha256=INnlb8THwC0kbUJY94FVFq4UtDRGRyBBm6jASTzV0mU,3111
|
|
4
4
|
canvas_cli/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -70,18 +70,19 @@ canvas_cli/apps/logs/logs.py,sha256=BFpZ-2OF2Rs1EMLePo5UjqC9fKQeqm8qZobNTFNCL_M,
|
|
|
70
70
|
canvas_cli/apps/plugin/__init__.py,sha256=GB6hBwbajm5cOs-DbJh3q6smPfAaIMa99tSbkvDtbqs,341
|
|
71
71
|
canvas_cli/apps/plugin/plugin.py,sha256=vlnd61nPt98NnYUPAeZMogcRPgv1pdDqBtey13H_Tug,20817
|
|
72
72
|
canvas_cli/apps/run_plugins/__init__.py,sha256=iAMgX_6D3CdjQodGx_azwhSjouaxquOm8Z8QVXnlTFE,117
|
|
73
|
-
canvas_cli/apps/run_plugins/run_plugins.py,sha256=
|
|
73
|
+
canvas_cli/apps/run_plugins/run_plugins.py,sha256=5JbO0l4f2xtJbSTomJgKUqj416sT9GcDF0TrDg2JZYQ,3275
|
|
74
74
|
canvas_cli/templates/plugins/application/cookiecutter.json,sha256=cI4Wpj68TkKeBP3P16PrjKacTHzsTIpl_rDdzyUpwz4,129
|
|
75
75
|
canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json,sha256=-mfqhpljpF4eE8g05pIEnZMt_bDUdhgIBtuQd-devv8,945
|
|
76
76
|
canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/README.md,sha256=3QKoJQq3YmdplGnDOBMsLCJ3Ya1_aKjoz-QiWc4QfjA,291
|
|
77
77
|
canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/applications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
78
78
|
canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/applications/my_application.py,sha256=rTUUycVOZEj5B4CoBqWMl9T4xPIyTqxsWGzXfAd6huY,584
|
|
79
79
|
canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/assets/python-logo.png,sha256=QAipNLQVwVK_pRO-PpsF3kISrkFUmifyxsjUGzokpNo,20637
|
|
80
|
-
canvas_cli/templates/plugins/default/cookiecutter.json,sha256=
|
|
81
|
-
canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/
|
|
82
|
-
canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/
|
|
83
|
-
canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/
|
|
84
|
-
canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/
|
|
80
|
+
canvas_cli/templates/plugins/default/cookiecutter.json,sha256=LbcuMaWxrLats1BffuDkk2192qNTgA-NU6sAnsFyj8M,201
|
|
81
|
+
canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/pyproject.toml,sha256=ogQ94GnE-Aj2KJbiTevwnomqKdZUeBZfIOTcU_KPPdI,422
|
|
82
|
+
canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/{{ cookiecutter.__package_name }}/CANVAS_MANIFEST.json,sha256=1keK1aJ-rJu4hm5ypqJT82hooCUTUOQ10dhzQ-mab7c,638
|
|
83
|
+
canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/{{ cookiecutter.__package_name }}/README.md,sha256=3QKoJQq3YmdplGnDOBMsLCJ3Ya1_aKjoz-QiWc4QfjA,291
|
|
84
|
+
canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/{{ cookiecutter.__package_name }}/protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
85
|
+
canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/{{ cookiecutter.__package_name }}/protocols/my_protocol.py,sha256=fKLLcOIwvSWenW8-7tr8VqnF4Iox_5wU9V-Qw9UySsA,2381
|
|
85
86
|
canvas_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
86
87
|
canvas_cli/utils/context/__init__.py,sha256=HhYvI-hydP0mV18nJiU7uo5gk0yN7EYNgouxieoGDOE,102
|
|
87
88
|
canvas_cli/utils/context/context.py,sha256=wk4TxlslF52uD9nXcEZ1eY8L1rcEHk7k-6YBVwaWnVY,5191
|
|
@@ -223,6 +224,10 @@ canvas_sdk/questionnaires/__init__.py,sha256=Pe3R-RY70AOllljO_caNAPpHpJfA3ML5zmw
|
|
|
223
224
|
canvas_sdk/questionnaires/utils.py,sha256=avDMUcQ5XmLfhlBpPJNMtNrVqrPy7g7HtqbSzztuIeQ,3666
|
|
224
225
|
canvas_sdk/templates/__init__.py,sha256=LtUmLDsGSTq249T6sA7PUzkl2OfCAyvtIOGPNHr6wTo,83
|
|
225
226
|
canvas_sdk/templates/utils.py,sha256=J-wDQ9ijOr_jz1SJxjpyZgR7JwXqtGMSS5IperLi2qE,1475
|
|
227
|
+
canvas_sdk/test_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
228
|
+
canvas_sdk/test_utils/factories/__init__.py,sha256=mtd247ZUaW3-w5RfuXGMC2iM8XYLnxXUIzbDWqSvo7o,171
|
|
229
|
+
canvas_sdk/test_utils/factories/patient.py,sha256=KKHUcgqu3r3sOc21gQ5p__71zBLm9QZysW5TSnJ631U,1138
|
|
230
|
+
canvas_sdk/test_utils/factories/user.py,sha256=QLxilvK22fHSsGFxe1j354VdMrdquO8nyWApdXXdS8Q,306
|
|
226
231
|
canvas_sdk/utils/__init__.py,sha256=PADveJMf-BtOapK5Cczy-nOuZzVh-HT2H6sfJW_SK9U,198
|
|
227
232
|
canvas_sdk/utils/db.py,sha256=wOZORBfyBVxQkay5vyEtQ4i0KVCfASFGREmecHMR8DE,409
|
|
228
233
|
canvas_sdk/utils/http.py,sha256=BUpDcc8s_MVViiM9Y3FOAMZWW8bj2a8zIuQCl8bXt_s,10729
|
|
@@ -261,7 +266,7 @@ canvas_sdk/v1/data/message.py,sha256=EKc2kkWGuzrgBH3sDUa9jiNpRP7J1ZdWE6BnPhY2gvs
|
|
|
261
266
|
canvas_sdk/v1/data/note.py,sha256=ZZN0CoQbXmXxzo1cb7bpiXBcy7kLpTzZueGgdd6H95Q,9301
|
|
262
267
|
canvas_sdk/v1/data/observation.py,sha256=l1EXGQ8FBBInxQthsmY1Oms9v73ni_J7SAP5M8Hmkvg,3881
|
|
263
268
|
canvas_sdk/v1/data/organization.py,sha256=gt0KZC1hklwWf57vCxs2K0qPnLpMyyNFWNL7-WZ6AqI,1159
|
|
264
|
-
canvas_sdk/v1/data/patient.py,sha256=
|
|
269
|
+
canvas_sdk/v1/data/patient.py,sha256=7D_bxeCAoA_I8-2ZLzAMkCrV9ziZ_-IND9uOq0Wp83I,10515
|
|
265
270
|
canvas_sdk/v1/data/patient_consent.py,sha256=zx1C__E6On-MC7OnTgEeP50tn77R6UXw15wC125jDog,2704
|
|
266
271
|
canvas_sdk/v1/data/payment_collection.py,sha256=h17qmQkCCwSdukIeeYMqBHZnpxpahOa03ckMHmgZV-I,954
|
|
267
272
|
canvas_sdk/v1/data/payor_specific_charge.py,sha256=TtByy7-QF00PfONC-QDo2kkxRAOdLaRjkhssQBPvZjw,770
|
|
@@ -275,8 +280,8 @@ canvas_sdk/v1/data/service_provider.py,sha256=LQfQ0esg97NpmSYnftsWqYUJyJIQOZKmpZ
|
|
|
275
280
|
canvas_sdk/v1/data/staff.py,sha256=iVqBXwANp1qYEL39DIpveKbuqd1nw1oSDQ917xw8xgk,8302
|
|
276
281
|
canvas_sdk/v1/data/task.py,sha256=9jSqr7e7ODJTs4hanWsOD-1Rb8gByJOuLta90V9V_hY,3480
|
|
277
282
|
canvas_sdk/v1/data/team.py,sha256=J4s98sS7UuUw9jJCe-_7eikr1dV4SQARDzK5oxmsO_o,2832
|
|
278
|
-
canvas_sdk/v1/data/user.py,sha256=
|
|
279
|
-
canvas_sdk/v1/data/utils.py,sha256=
|
|
283
|
+
canvas_sdk/v1/data/user.py,sha256=82jnqVrYeWo2eBXY_MH2r75LZo5EKgY9kalahrUl0_s,511
|
|
284
|
+
canvas_sdk/v1/data/utils.py,sha256=t_Q2wpTnvJBx3AJeGD_7ymLNQG63J3E3LU5uq_O99bQ,899
|
|
280
285
|
canvas_sdk/value_set/__init__.py,sha256=YYXr5tEQlnwMgTXJbOG963tKSPiUOM4mUkX8wuiqp7U,17
|
|
281
286
|
canvas_sdk/value_set/custom.py,sha256=smf5fnPwuW-7H_B49CUfLWnq1QH-PhItvYw0siVsu-o,20173
|
|
282
287
|
canvas_sdk/value_set/hcc2018.py,sha256=MMudhqOOgvedh8-dDfC_JbHJn-sNRbk8Q357agD_Pww,2198496
|
|
@@ -309,7 +314,7 @@ plugin_runner/exceptions.py,sha256=ltqn56SMTg-T5miSh5hux4ojwx0hZGSWaB7BxyAmcAo,5
|
|
|
309
314
|
plugin_runner/generate_allowed_imports.py,sha256=LQuVxL_j5n0Sj-KgR4Q8D9mj0xfuDqzO69kBfZUqwGE,2565
|
|
310
315
|
plugin_runner/installation.py,sha256=2KTDWWsQ97WIN2k9tC4d50zN77WWK_1D5obXlhLfWw8,8536
|
|
311
316
|
plugin_runner/load_all_plugins.py,sha256=4T2gW2YljhIx4xfwf1c0F_8oIbE1ubsLj0ShkHRtlVY,5847
|
|
312
|
-
plugin_runner/plugin_runner.py,sha256=
|
|
317
|
+
plugin_runner/plugin_runner.py,sha256=RpcZoXXzGEflJGuHJaAOCP24mWb4fG1bLw3ryA6n88k,26623
|
|
313
318
|
plugin_runner/sandbox.py,sha256=A5iaxQePwA5FERK232MArq2VzZzHKM5ydrfTDFeXcLg,30232
|
|
314
319
|
protobufs/canvas_generated/messages/effects.proto,sha256=wQBRk0_XN8ssIjDtRttxsEG6pZJbYS8czGzdvpx3g6M,9756
|
|
315
320
|
protobufs/canvas_generated/messages/events.proto,sha256=21qM3Ct9-iIG_T7O-XoFPWpnVafocYJ1KGiZ2nn1CFM,52047
|
|
@@ -317,7 +322,7 @@ protobufs/canvas_generated/messages/plugins.proto,sha256=xJyEeTwM6wWja3vGECLsIzf
|
|
|
317
322
|
protobufs/canvas_generated/services/plugin_runner.proto,sha256=PZ0Ts11b9tdA5Gkg2M05JVEKAm0R4LFEwrGRS-TQ16E,466
|
|
318
323
|
pubsub/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
319
324
|
pubsub/pubsub.py,sha256=PHIvJ5SD3M-jQSYeGGSj1FuG6CvP6BQffAoGax9Uudk,1423
|
|
320
|
-
canvas-0.
|
|
321
|
-
canvas-0.
|
|
322
|
-
canvas-0.
|
|
323
|
-
canvas-0.
|
|
325
|
+
canvas-0.58.0.dist-info/METADATA,sha256=QsN4_J0x0w2_4Hg8lQLY6tl2CwWLPgv-8NJzffQH-jg,4758
|
|
326
|
+
canvas-0.58.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
327
|
+
canvas-0.58.0.dist-info/entry_points.txt,sha256=0Vs_9GmTVUNniH6eDBlRPgofmADMV4BES6Ao26M4AbM,47
|
|
328
|
+
canvas-0.58.0.dist-info/RECORD,,
|
|
@@ -1,16 +1,102 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import django.db
|
|
6
|
+
import typer
|
|
7
|
+
from django.core.management import call_command
|
|
8
|
+
|
|
9
|
+
import settings
|
|
1
10
|
from plugin_runner.plugin_runner import main as run_server
|
|
2
11
|
|
|
3
12
|
|
|
4
|
-
|
|
13
|
+
@contextmanager
|
|
14
|
+
def _patch_default_db_connection() -> Generator[None, None, None]:
|
|
15
|
+
"""
|
|
16
|
+
Temporarily patch the 'default' Django DB connection to allow db writes.
|
|
17
|
+
"""
|
|
18
|
+
original_default = django.db.connections["default"]
|
|
19
|
+
try:
|
|
20
|
+
temp_handler = django.db.utils.ConnectionHandler(
|
|
21
|
+
{"default": settings.SQLITE_WRITE_MODE_DATABASE}
|
|
22
|
+
)
|
|
23
|
+
django.db.connections["default"] = temp_handler["default"]
|
|
24
|
+
yield
|
|
25
|
+
finally:
|
|
26
|
+
django.db.connections["default"] = original_default
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _run_db_seed_file(path: Path) -> None:
|
|
30
|
+
"""Run the database setup file to initialize the database."""
|
|
31
|
+
code = path.read_text()
|
|
32
|
+
exec_globals = {"__name__": "__main__"}
|
|
33
|
+
exec(code, exec_globals)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _reset_db(seed_file: Path | None = None) -> None:
|
|
37
|
+
"""Reset the database."""
|
|
38
|
+
settings.SQLITE_DB_PATH.unlink(missing_ok=True)
|
|
39
|
+
|
|
40
|
+
with _patch_default_db_connection():
|
|
41
|
+
call_command("migrate", run_syncdb=True, verbosity=0)
|
|
42
|
+
if seed_file:
|
|
43
|
+
_run_db_seed_file(seed_file)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_plugin(
|
|
47
|
+
plugin_directory: str = typer.Argument(..., help="Path to the plugin directory to run."),
|
|
48
|
+
db_seed_file: Path | None = typer.Option(
|
|
49
|
+
help="Path to the database seed file to use.",
|
|
50
|
+
default=None,
|
|
51
|
+
),
|
|
52
|
+
reset_db: bool = typer.Option(
|
|
53
|
+
help="Reset the database before running the plugin.", default=False
|
|
54
|
+
),
|
|
55
|
+
) -> None:
|
|
5
56
|
"""
|
|
6
57
|
Run the specified plugin for local development.
|
|
7
58
|
"""
|
|
8
|
-
|
|
59
|
+
_run_plugins([plugin_directory], db_seed_file=db_seed_file, reset_db=reset_db)
|
|
9
60
|
|
|
10
61
|
|
|
11
|
-
def run_plugins(
|
|
62
|
+
def run_plugins(
|
|
63
|
+
plugin_directories: list[str],
|
|
64
|
+
db_seed_file: Path | None = typer.Option(
|
|
65
|
+
help="Path to the database seed file to use.",
|
|
66
|
+
default=None,
|
|
67
|
+
),
|
|
68
|
+
reset_db: bool = typer.Option(
|
|
69
|
+
help="Reset the database before running the plugin(s).", default=False
|
|
70
|
+
),
|
|
71
|
+
) -> None:
|
|
12
72
|
"""
|
|
13
73
|
Run the specified plugins for local development.
|
|
14
74
|
"""
|
|
75
|
+
_run_plugins(plugin_directories, db_seed_file=db_seed_file, reset_db=reset_db)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _run_plugins(
|
|
79
|
+
plugin_directories: list[str], db_seed_file: Path | None = None, reset_db: bool = False
|
|
80
|
+
) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Run the specified plugins for local development.
|
|
83
|
+
"""
|
|
84
|
+
if db_seed_file:
|
|
85
|
+
if not db_seed_file.exists():
|
|
86
|
+
raise typer.BadParameter(f"Database setup file not found: {db_seed_file.resolve()}")
|
|
87
|
+
if not db_seed_file.is_file():
|
|
88
|
+
raise typer.BadParameter(
|
|
89
|
+
f"Database setup file is not a regular file: {db_seed_file.resolve()}"
|
|
90
|
+
)
|
|
91
|
+
if not db_seed_file.suffix == ".py":
|
|
92
|
+
raise typer.BadParameter("Database setup file must be a Python script (.py)")
|
|
93
|
+
|
|
94
|
+
if settings.CANVAS_SDK_DB_BACKEND != "sqlite3":
|
|
95
|
+
raise typer.BadParameter(
|
|
96
|
+
"Database backend must be 'sqlite3' for local plugin development. Please unset 'DATABASE_URL' env var."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if db_seed_file or reset_db:
|
|
100
|
+
_reset_db(seed_file=db_seed_file)
|
|
101
|
+
|
|
15
102
|
run_server(plugin_directories)
|
|
16
|
-
return
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"project_name": "My Cool Plugin",
|
|
3
|
-
"__project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '
|
|
3
|
+
"__project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}",
|
|
4
|
+
"__package_name": "{{ cookiecutter.__project_slug.replace('-', '_') }}"
|
|
4
5
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
authors = [
|
|
3
|
+
{email = "engineering@canvasmedical.com", name = "Canvas Team"}
|
|
4
|
+
]
|
|
5
|
+
dependencies = [
|
|
6
|
+
"canvas[test-utils]"
|
|
7
|
+
]
|
|
8
|
+
description = "Some description of your project."
|
|
9
|
+
license = "MIT"
|
|
10
|
+
name = "{{ cookiecutter.__project_slug }}"
|
|
11
|
+
readme = "{{cookiecutter.__package_name }}/README.md"
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
version = "0.1.0"
|
|
14
|
+
|
|
15
|
+
[tool.pytest.ini_options]
|
|
16
|
+
python_files = ["*_tests.py", "test_*.py", "tests.py"]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sdk_version": "0.1.4",
|
|
3
3
|
"plugin_version": "0.0.1",
|
|
4
|
-
"name": "{{ cookiecutter.
|
|
4
|
+
"name": "{{ cookiecutter.__package_name }}",
|
|
5
5
|
"description": "Edit the description in CANVAS_MANIFEST.json",
|
|
6
6
|
"components": {
|
|
7
7
|
"protocols": [
|
|
8
8
|
{
|
|
9
|
-
"class": "{{ cookiecutter.
|
|
9
|
+
"class": "{{ cookiecutter.__package_name }}.protocols.my_protocol:Protocol",
|
|
10
10
|
"description": "A protocol that does xyz..."
|
|
11
11
|
}
|
|
12
12
|
],
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
import factory
|
|
4
|
+
from factory.fuzzy import FuzzyDate
|
|
5
|
+
|
|
6
|
+
from canvas_sdk.test_utils.factories.user import CanvasUserFactory
|
|
7
|
+
from canvas_sdk.v1.data import Patient, PatientAddress
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PatientAddressFactory(factory.django.DjangoModelFactory[PatientAddress]):
|
|
11
|
+
"""Factory for creating a PatientAddress."""
|
|
12
|
+
|
|
13
|
+
class Meta:
|
|
14
|
+
model = PatientAddress
|
|
15
|
+
|
|
16
|
+
line1 = "1234 Main Street"
|
|
17
|
+
line2 = "Apt 3"
|
|
18
|
+
city = "San Francisco"
|
|
19
|
+
district = "Sunset"
|
|
20
|
+
state_code = "CA"
|
|
21
|
+
postal_code = "94112"
|
|
22
|
+
country = "USA"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PatientFactory(factory.django.DjangoModelFactory[Patient]):
|
|
26
|
+
"""Factory for creating a Patient."""
|
|
27
|
+
|
|
28
|
+
class Meta:
|
|
29
|
+
model = Patient
|
|
30
|
+
|
|
31
|
+
birth_date = FuzzyDate(
|
|
32
|
+
start_date=datetime.date.today() - datetime.timedelta(days=100 * 365),
|
|
33
|
+
end_date=datetime.date.today() - datetime.timedelta(days=10 * 365),
|
|
34
|
+
)
|
|
35
|
+
first_name = factory.Faker("first_name")
|
|
36
|
+
middle_name = factory.Faker("first_name")
|
|
37
|
+
last_name = factory.Faker("last_name")
|
|
38
|
+
addresses = factory.RelatedFactory(PatientAddressFactory, "patient")
|
|
39
|
+
user = factory.SubFactory(CanvasUserFactory)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import factory
|
|
2
|
+
|
|
3
|
+
from canvas_sdk.v1.data import CanvasUser
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CanvasUserFactory(factory.django.DjangoModelFactory[CanvasUser]):
|
|
7
|
+
"""Factory for creating a CanvasUser."""
|
|
8
|
+
|
|
9
|
+
class Meta:
|
|
10
|
+
model = CanvasUser
|
|
11
|
+
|
|
12
|
+
email = factory.Faker("email")
|
|
13
|
+
phone_number = factory.Faker("phone_number")
|
canvas_sdk/v1/data/patient.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
5
|
import arrow
|
|
6
6
|
from django.contrib.postgres.fields import ArrayField
|
|
@@ -16,10 +16,7 @@ from canvas_sdk.v1.data.common import (
|
|
|
16
16
|
ContactPointSystem,
|
|
17
17
|
ContactPointUse,
|
|
18
18
|
)
|
|
19
|
-
from canvas_sdk.v1.data.utils import create_key
|
|
20
|
-
|
|
21
|
-
if TYPE_CHECKING:
|
|
22
|
-
from django_stubs_ext.db.models.manager import RelatedManager
|
|
19
|
+
from canvas_sdk.v1.data.utils import create_key, generate_mrn
|
|
23
20
|
|
|
24
21
|
|
|
25
22
|
class SexAtBirth(TextChoices):
|
|
@@ -51,45 +48,65 @@ class Patient(Model):
|
|
|
51
48
|
id = models.CharField(
|
|
52
49
|
max_length=32, db_column="key", unique=True, editable=False, default=create_key
|
|
53
50
|
)
|
|
54
|
-
first_name = models.CharField(max_length=255)
|
|
55
|
-
|
|
51
|
+
first_name = models.CharField(max_length=255, default="", blank=True)
|
|
52
|
+
middle_name = models.CharField(max_length=255, default="", blank=True)
|
|
53
|
+
last_name = models.CharField(max_length=255, default="", blank=True)
|
|
54
|
+
maiden_name = models.CharField(max_length=255, default="", blank=True)
|
|
56
55
|
birth_date = models.DateField()
|
|
57
56
|
business_line = models.ForeignKey(
|
|
58
|
-
"v1.BusinessLine",
|
|
57
|
+
"v1.BusinessLine",
|
|
58
|
+
on_delete=models.DO_NOTHING,
|
|
59
|
+
related_name="patients",
|
|
60
|
+
null=True,
|
|
59
61
|
)
|
|
60
62
|
sex_at_birth = models.CharField(choices=SexAtBirth.choices, max_length=3)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
suffix = models.CharField(max_length=100)
|
|
65
|
-
middle_name = models.CharField(max_length=255)
|
|
66
|
-
maiden_name = models.CharField(max_length=255)
|
|
63
|
+
|
|
64
|
+
prefix = models.CharField(max_length=100, blank=True, default="")
|
|
65
|
+
suffix = models.CharField(max_length=100, blank=True, default="")
|
|
67
66
|
nickname = models.CharField(max_length=255)
|
|
68
|
-
sexual_orientation_term = models.CharField(max_length=255)
|
|
69
|
-
sexual_orientation_code = models.CharField(max_length=255)
|
|
70
|
-
gender_identity_term = models.CharField(max_length=255)
|
|
71
|
-
gender_identity_code = models.CharField(max_length=255)
|
|
72
|
-
preferred_pronouns = models.CharField(max_length=255)
|
|
73
|
-
biological_race_codes = ArrayField(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
67
|
+
sexual_orientation_term = models.CharField(max_length=255, default="", blank=True)
|
|
68
|
+
sexual_orientation_code = models.CharField(max_length=255, default="", blank=True)
|
|
69
|
+
gender_identity_term = models.CharField(max_length=255, default="", blank=True)
|
|
70
|
+
gender_identity_code = models.CharField(max_length=255, default="", blank=True)
|
|
71
|
+
preferred_pronouns = models.CharField(max_length=255, default="", blank=True)
|
|
72
|
+
biological_race_codes = ArrayField(
|
|
73
|
+
models.CharField(max_length=100, default="", blank=True), default=list, blank=True
|
|
74
|
+
)
|
|
75
|
+
last_known_timezone = models.CharField(max_length=32, null=True, blank=True)
|
|
76
|
+
mrn = models.CharField(max_length=9, unique=True, default=generate_mrn)
|
|
77
|
+
active = models.BooleanField(default=True)
|
|
78
|
+
deceased = models.BooleanField(default=False)
|
|
79
|
+
deceased_datetime = models.DateTimeField(null=True, blank=True)
|
|
80
|
+
deceased_cause = models.TextField(default="", blank=True)
|
|
81
|
+
deceased_comment = models.TextField(default="", blank=True)
|
|
82
|
+
other_gender_description = models.CharField(max_length=255, blank=True, default="")
|
|
83
|
+
social_security_number = models.CharField(max_length=9, blank=True, default="")
|
|
84
|
+
administrative_note = models.TextField(null=True, blank=True)
|
|
85
|
+
clinical_note = models.TextField(default="", blank=True)
|
|
86
|
+
mothers_maiden_name = models.CharField(max_length=255, blank=True, default="")
|
|
87
|
+
multiple_birth_indicator = models.BooleanField(null=True, blank=True)
|
|
88
|
+
birth_order = models.BigIntegerField(null=True, blank=True)
|
|
89
|
+
|
|
90
|
+
default_location = models.ForeignKey(
|
|
91
|
+
"v1.PracticeLocation",
|
|
92
|
+
on_delete=models.SET_NULL,
|
|
93
|
+
default=None,
|
|
94
|
+
null=True,
|
|
95
|
+
blank=True,
|
|
96
|
+
related_name="default_patients",
|
|
97
|
+
)
|
|
98
|
+
default_provider = models.ForeignKey(
|
|
99
|
+
"v1.Staff",
|
|
100
|
+
on_delete=models.SET_NULL,
|
|
101
|
+
default=None,
|
|
102
|
+
null=True,
|
|
103
|
+
blank=True,
|
|
104
|
+
related_name="default_patients",
|
|
105
|
+
)
|
|
90
106
|
user = models.ForeignKey("v1.CanvasUser", on_delete=models.DO_NOTHING, null=True)
|
|
91
107
|
|
|
92
|
-
|
|
108
|
+
created = models.DateTimeField(auto_now_add=True)
|
|
109
|
+
modified = models.DateTimeField(auto_now=True)
|
|
93
110
|
|
|
94
111
|
@classmethod
|
|
95
112
|
def find(cls, id: str) -> Patient:
|
|
@@ -179,20 +196,23 @@ class PatientAddress(IdentifiableModel):
|
|
|
179
196
|
class Meta:
|
|
180
197
|
db_table = "canvas_sdk_data_api_patientaddress_001"
|
|
181
198
|
|
|
182
|
-
line1 = models.CharField(max_length=255)
|
|
183
|
-
line2 = models.CharField(max_length=255)
|
|
199
|
+
line1 = models.CharField(max_length=255, default="", blank=True)
|
|
200
|
+
line2 = models.CharField(max_length=255, default="", blank=True)
|
|
184
201
|
city = models.CharField(max_length=255)
|
|
185
|
-
district = models.CharField(max_length=255)
|
|
202
|
+
district = models.CharField(max_length=255, blank=True, default="")
|
|
186
203
|
state_code = models.CharField(max_length=2)
|
|
187
204
|
postal_code = models.CharField(max_length=255)
|
|
188
|
-
use = models.CharField(choices=AddressUse.choices, max_length=10)
|
|
189
|
-
type = models.CharField(choices=AddressType.choices, max_length=10)
|
|
190
|
-
longitude = models.FloatField()
|
|
191
|
-
latitude = models.FloatField()
|
|
192
|
-
start = models.DateField()
|
|
193
|
-
end = models.DateField()
|
|
205
|
+
use = models.CharField(choices=AddressUse.choices, max_length=10, default=AddressUse.HOME)
|
|
206
|
+
type = models.CharField(choices=AddressType.choices, max_length=10, default=AddressType.BOTH)
|
|
207
|
+
longitude = models.FloatField(null=True, blank=True)
|
|
208
|
+
latitude = models.FloatField(null=True, blank=True)
|
|
209
|
+
start = models.DateField(null=True, blank=True)
|
|
210
|
+
end = models.DateField(null=True, blank=True)
|
|
194
211
|
country = models.CharField(max_length=255)
|
|
195
|
-
state = models.CharField(
|
|
212
|
+
state = models.CharField(
|
|
213
|
+
choices=AddressState.choices, max_length=20, default=AddressState.ACTIVE
|
|
214
|
+
)
|
|
215
|
+
|
|
196
216
|
patient = models.ForeignKey(
|
|
197
217
|
"v1.Patient", on_delete=models.DO_NOTHING, related_name="addresses", null=True
|
|
198
218
|
)
|
canvas_sdk/v1/data/user.py
CHANGED
|
@@ -11,8 +11,8 @@ class CanvasUser(Model):
|
|
|
11
11
|
|
|
12
12
|
email = models.EmailField(db_column="email")
|
|
13
13
|
phone_number = models.CharField(db_column="phone_number", max_length=255)
|
|
14
|
-
last_invite_date_time = models.DateTimeField()
|
|
15
|
-
is_portal_registered = models.BooleanField()
|
|
14
|
+
last_invite_date_time = models.DateTimeField(null=True, blank=True)
|
|
15
|
+
is_portal_registered = models.BooleanField(default=False)
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
__exports__ = ("CanvasUser",)
|
canvas_sdk/v1/data/utils.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import string
|
|
1
3
|
import uuid
|
|
2
4
|
from collections.abc import Sequence
|
|
3
5
|
from decimal import Decimal
|
|
@@ -13,4 +15,18 @@ def create_key() -> str:
|
|
|
13
15
|
return uuid.uuid4().hex
|
|
14
16
|
|
|
15
17
|
|
|
18
|
+
def generate_mrn(length: int = 9, max_attempts: int = 100) -> str:
|
|
19
|
+
"""Generates a unique Medical Record Number (MRN) of specified length."""
|
|
20
|
+
from canvas_sdk.v1.data import Patient
|
|
21
|
+
|
|
22
|
+
digits = string.digits
|
|
23
|
+
|
|
24
|
+
for _ in range(max_attempts):
|
|
25
|
+
mrn = "".join(random.choices(digits, k=length))
|
|
26
|
+
if not Patient.objects.filter(mrn=mrn).exists():
|
|
27
|
+
return mrn
|
|
28
|
+
|
|
29
|
+
raise RuntimeError(f"Unable to generate a unique MRN after {max_attempts} attempts")
|
|
30
|
+
|
|
31
|
+
|
|
16
32
|
__exports__ = ()
|
plugin_runner/plugin_runner.py
CHANGED
|
@@ -731,7 +731,14 @@ def main(specified_plugin_paths: list[str] | None = None) -> None:
|
|
|
731
731
|
port = "50051"
|
|
732
732
|
|
|
733
733
|
executor = ThreadPoolExecutor(max_workers=settings.PLUGIN_RUNNER_MAX_WORKERS)
|
|
734
|
-
server = grpc.server(
|
|
734
|
+
server = grpc.server(
|
|
735
|
+
thread_pool=executor,
|
|
736
|
+
options=(
|
|
737
|
+
# set max message lengths to 64mb
|
|
738
|
+
("grpc.max_receive_message_length", 64 * 1024 * 1024),
|
|
739
|
+
("grpc.max_send_message_length", 64 * 1024 * 1024),
|
|
740
|
+
),
|
|
741
|
+
)
|
|
735
742
|
server.add_insecure_port("127.0.0.1:" + port)
|
|
736
743
|
|
|
737
744
|
add_PluginRunnerServicer_to_server(PluginRunner(), server)
|
settings.py
CHANGED
|
@@ -108,18 +108,21 @@ if CANVAS_SDK_DB_BACKEND == "postgres":
|
|
|
108
108
|
DATABASES = {"default": db_config}
|
|
109
109
|
|
|
110
110
|
elif CANVAS_SDK_DB_BACKEND == "sqlite3":
|
|
111
|
+
SQLITE_DB_PATH = BASE_DIR / "canvas_db.sqlite3"
|
|
112
|
+
SQLITE_WRITE_MODE_DATABASE = {
|
|
113
|
+
"ENGINE": "django.db.backends.sqlite3",
|
|
114
|
+
"NAME": str(SQLITE_DB_PATH),
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
DATABASES = {
|
|
112
118
|
"default": {
|
|
113
119
|
"ENGINE": "django.db.backends.sqlite3",
|
|
114
|
-
"NAME": f"file:{
|
|
120
|
+
"NAME": f"file:{SQLITE_DB_PATH}?mode=ro",
|
|
115
121
|
"OPTIONS": {"uri": True},
|
|
116
|
-
}
|
|
117
|
-
"default-write": {
|
|
118
|
-
"ENGINE": "django.db.backends.sqlite3",
|
|
119
|
-
"NAME": str(BASE_DIR / "db.sqlite3"),
|
|
120
|
-
},
|
|
122
|
+
}
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
|
|
123
126
|
else:
|
|
124
127
|
raise ImproperlyConfigured(
|
|
125
128
|
"Unsupported database backend specified in 'CANVAS_SDK_DB_BACKEND' setting. "
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|