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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: canvas
3
- Version: 0.57.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
  [![codecov](https://codecov.io/gh/canvas-medical/canvas-plugins/graph/badge.svg?token=P8JJUOJ8FH)](https://codecov.io/gh/canvas-medical/canvas-plugins)
@@ -1,4 +1,4 @@
1
- settings.py,sha256=Yu4LE0PcohXgxqO8-W0b5uoXMJ20p-NpUj1lNFCdB8I,6296
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=w4JTsXFiHckZahvbSOJAqZo3jWwedxlHLiXct1j_6cY,392
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=dWEB3wJ8U4bko8jX26PgLLg_jgWlafLTNqsGnY1PUcg,124
81
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json,sha256=3a5PsKPZcENxRd0FYG9AnXNOKlpzP-hQua4HohIL3v0,638
82
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/README.md,sha256=3QKoJQq3YmdplGnDOBMsLCJ3Ya1_aKjoz-QiWc4QfjA,291
83
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py,sha256=fKLLcOIwvSWenW8-7tr8VqnF4Iox_5wU9V-Qw9UySsA,2381
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=2leEjD8tDJD4AgSXl8rtEimyKMoSxgaqqKRqTXKz11w,9405
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=14_yDFXQ38DF7quZLY5pXmL90_0wfYUTZvEkfKEAgAY,477
279
- canvas_sdk/v1/data/utils.py,sha256=r9eXGD-mxGhTmHaGEn_ap_QTE6vH1H4SFOdkb9wZNzM,392
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=oGYaYox_yG45N0IQ2DjgMMCpNEP_aGCwClIeadVFJ8Q,26402
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.57.0.dist-info/METADATA,sha256=KZkuAbo0NwR-D6ZfZZgukyJDe00ornxC-Ep8RDkz0gQ,4645
321
- canvas-0.57.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
322
- canvas-0.57.0.dist-info/entry_points.txt,sha256=0Vs_9GmTVUNniH6eDBlRPgofmADMV4BES6Ao26M4AbM,47
323
- canvas-0.57.0.dist-info/RECORD,,
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
- def run_plugin(plugin_directory: str) -> None:
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
- return run_plugins([plugin_directory])
59
+ _run_plugins([plugin_directory], db_seed_file=db_seed_file, reset_db=reset_db)
9
60
 
10
61
 
11
- def run_plugins(plugin_directories: list[str]) -> None:
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.__project_slug }}",
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.__project_slug }}.protocols.my_protocol:Protocol",
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,4 @@
1
+ from .patient import PatientAddressFactory, PatientFactory
2
+ from .user import CanvasUserFactory
3
+
4
+ __all__ = ("CanvasUserFactory", "PatientAddressFactory", "PatientFactory")
@@ -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")
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any
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
- last_name = models.CharField(max_length=255)
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", on_delete=models.DO_NOTHING, related_name="patients"
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
- created = models.DateTimeField(auto_now_add=True)
62
- modified = models.DateTimeField(auto_now=True)
63
- prefix = models.CharField(max_length=100)
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(models.CharField(max_length=100))
74
- last_known_timezone = models.CharField(max_length=32)
75
- mrn = models.CharField(max_length=9)
76
- active = models.BooleanField()
77
- deceased = models.BooleanField()
78
- deceased_datetime = models.DateTimeField()
79
- deceased_cause = models.TextField()
80
- deceased_comment = models.TextField()
81
- other_gender_description = models.CharField(max_length=255)
82
- social_security_number = models.CharField(max_length=9)
83
- administrative_note = models.TextField()
84
- clinical_note = models.TextField()
85
- mothers_maiden_name = models.CharField(max_length=255)
86
- multiple_birth_indicator = models.BooleanField()
87
- birth_order = models.BigIntegerField()
88
- default_location_id = models.BigIntegerField()
89
- default_provider_id = models.BigIntegerField()
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
- settings: RelatedManager[PatientSetting]
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(choices=AddressState.choices, max_length=20)
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
  )
@@ -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",)
@@ -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__ = ()
@@ -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(thread_pool=executor)
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:{str(BASE_DIR / 'db.sqlite3')}?mode=ro",
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. "