canvas 0.11.1__py3-none-any.whl → 0.13.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.11.1.dist-info → canvas-0.13.0.dist-info}/METADATA +1 -1
- {canvas-0.11.1.dist-info → canvas-0.13.0.dist-info}/RECORD +38 -20
- canvas_cli/apps/plugin/plugin.py +13 -4
- canvas_cli/templates/plugins/application/cookiecutter.json +4 -0
- canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json +28 -0
- canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/README.md +11 -0
- canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/applications/__init__.py +0 -0
- canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/applications/my_application.py +12 -0
- canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/assets/python-logo.png +0 -0
- canvas_cli/utils/validators/manifest_schema.py +18 -1
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +36 -0
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +10 -0
- canvas_sdk/commands/__init__.py +6 -0
- canvas_sdk/commands/commands/exam.py +9 -0
- canvas_sdk/commands/commands/review_of_systems.py +9 -0
- canvas_sdk/commands/commands/structured_assessment.py +9 -0
- canvas_sdk/effects/__init__.py +3 -1
- canvas_sdk/effects/launch_modal.py +24 -0
- canvas_sdk/effects/patient_portal/__init__.py +0 -0
- canvas_sdk/effects/patient_portal/intake_form_results.py +24 -0
- canvas_sdk/effects/show_button.py +28 -0
- canvas_sdk/handlers/action_button.py +55 -0
- canvas_sdk/handlers/application.py +29 -0
- canvas_sdk/handlers/base.py +8 -1
- canvas_sdk/protocols/base.py +3 -1
- canvas_sdk/v1/data/__init__.py +4 -0
- canvas_sdk/v1/data/note.py +3 -4
- canvas_sdk/v1/data/organization.py +29 -0
- canvas_sdk/v1/data/practicelocation.py +105 -0
- plugin_runner/plugin_runner.py +37 -23
- plugin_runner/sandbox.py +2 -6
- plugin_runner/tests/test_application.py +65 -0
- plugin_runner/tests/test_plugin_runner.py +4 -4
- plugin_runner/tests/test_sandbox.py +2 -2
- {canvas-0.11.1.dist-info → canvas-0.13.0.dist-info}/WHEEL +0 -0
- {canvas-0.11.1.dist-info → canvas-0.13.0.dist-info}/entry_points.txt +0 -0
|
@@ -647,6 +647,9 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
647
647
|
DEFER_CODING_GAP_COMMAND__PRE_EXECUTE_ACTION: _ClassVar[EventType]
|
|
648
648
|
DEFER_CODING_GAP_COMMAND__POST_EXECUTE_ACTION: _ClassVar[EventType]
|
|
649
649
|
DEFER_CODING_GAP_COMMAND__POST_INSERTED_INTO_NOTE: _ClassVar[EventType]
|
|
650
|
+
SHOW_NOTE_HEADER_BUTTON: _ClassVar[EventType]
|
|
651
|
+
SHOW_NOTE_FOOTER_BUTTON: _ClassVar[EventType]
|
|
652
|
+
ACTION_BUTTON_CLICKED: _ClassVar[EventType]
|
|
650
653
|
PATIENT_CHART__CONDITIONS: _ClassVar[EventType]
|
|
651
654
|
PATIENT_CHART_SUMMARY__SECTION_CONFIGURATION: _ClassVar[EventType]
|
|
652
655
|
PATIENT_PROFILE__SECTION_CONFIGURATION: _ClassVar[EventType]
|
|
@@ -654,6 +657,8 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
654
657
|
CLAIM__CONDITIONS: _ClassVar[EventType]
|
|
655
658
|
PLUGIN_CREATED: _ClassVar[EventType]
|
|
656
659
|
PLUGIN_UPDATED: _ClassVar[EventType]
|
|
660
|
+
APPLICATION__ON_OPEN: _ClassVar[EventType]
|
|
661
|
+
PATIENT_PORTAL__GET_INTAKE_FORMS: _ClassVar[EventType]
|
|
657
662
|
UNKNOWN: EventType
|
|
658
663
|
ALLERGY_INTOLERANCE_CREATED: EventType
|
|
659
664
|
ALLERGY_INTOLERANCE_UPDATED: EventType
|
|
@@ -1292,6 +1297,9 @@ DEFER_CODING_GAP_COMMAND__POST_ENTER_IN_ERROR: EventType
|
|
|
1292
1297
|
DEFER_CODING_GAP_COMMAND__PRE_EXECUTE_ACTION: EventType
|
|
1293
1298
|
DEFER_CODING_GAP_COMMAND__POST_EXECUTE_ACTION: EventType
|
|
1294
1299
|
DEFER_CODING_GAP_COMMAND__POST_INSERTED_INTO_NOTE: EventType
|
|
1300
|
+
SHOW_NOTE_HEADER_BUTTON: EventType
|
|
1301
|
+
SHOW_NOTE_FOOTER_BUTTON: EventType
|
|
1302
|
+
ACTION_BUTTON_CLICKED: EventType
|
|
1295
1303
|
PATIENT_CHART__CONDITIONS: EventType
|
|
1296
1304
|
PATIENT_CHART_SUMMARY__SECTION_CONFIGURATION: EventType
|
|
1297
1305
|
PATIENT_PROFILE__SECTION_CONFIGURATION: EventType
|
|
@@ -1299,6 +1307,8 @@ PATIENT_PROFILE__ADD_PHARMACY__POST_SEARCH: EventType
|
|
|
1299
1307
|
CLAIM__CONDITIONS: EventType
|
|
1300
1308
|
PLUGIN_CREATED: EventType
|
|
1301
1309
|
PLUGIN_UPDATED: EventType
|
|
1310
|
+
APPLICATION__ON_OPEN: EventType
|
|
1311
|
+
PATIENT_PORTAL__GET_INTAKE_FORMS: EventType
|
|
1302
1312
|
|
|
1303
1313
|
class Event(_message.Message):
|
|
1304
1314
|
__slots__ = ("type", "target", "context", "target_type")
|
canvas_sdk/commands/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@ from canvas_sdk.commands.commands.allergy import AllergyCommand
|
|
|
2
2
|
from canvas_sdk.commands.commands.assess import AssessCommand
|
|
3
3
|
from canvas_sdk.commands.commands.close_goal import CloseGoalCommand
|
|
4
4
|
from canvas_sdk.commands.commands.diagnose import DiagnoseCommand
|
|
5
|
+
from canvas_sdk.commands.commands.exam import PhysicalExamCommand
|
|
5
6
|
from canvas_sdk.commands.commands.family_history import FamilyHistoryCommand
|
|
6
7
|
from canvas_sdk.commands.commands.goal import GoalCommand
|
|
7
8
|
from canvas_sdk.commands.commands.history_present_illness import (
|
|
@@ -21,7 +22,9 @@ from canvas_sdk.commands.commands.questionnaire import QuestionnaireCommand
|
|
|
21
22
|
from canvas_sdk.commands.commands.reason_for_visit import ReasonForVisitCommand
|
|
22
23
|
from canvas_sdk.commands.commands.refill import RefillCommand
|
|
23
24
|
from canvas_sdk.commands.commands.remove_allergy import RemoveAllergyCommand
|
|
25
|
+
from canvas_sdk.commands.commands.review_of_systems import ReviewOfSystemsCommand
|
|
24
26
|
from canvas_sdk.commands.commands.stop_medication import StopMedicationCommand
|
|
27
|
+
from canvas_sdk.commands.commands.structured_assessment import StructuredAssessmentCommand
|
|
25
28
|
from canvas_sdk.commands.commands.task import TaskCommand
|
|
26
29
|
from canvas_sdk.commands.commands.update_diagnosis import UpdateDiagnosisCommand
|
|
27
30
|
from canvas_sdk.commands.commands.update_goal import UpdateGoalCommand
|
|
@@ -43,11 +46,14 @@ __all__ = (
|
|
|
43
46
|
"PerformCommand",
|
|
44
47
|
"PlanCommand",
|
|
45
48
|
"PrescribeCommand",
|
|
49
|
+
"PhysicalExamCommand",
|
|
46
50
|
"QuestionnaireCommand",
|
|
47
51
|
"ReasonForVisitCommand",
|
|
48
52
|
"RefillCommand",
|
|
49
53
|
"RemoveAllergyCommand",
|
|
54
|
+
"ReviewOfSystemsCommand",
|
|
50
55
|
"StopMedicationCommand",
|
|
56
|
+
"StructuredAssessmentCommand",
|
|
51
57
|
"TaskCommand",
|
|
52
58
|
"UpdateDiagnosisCommand",
|
|
53
59
|
"UpdateGoalCommand",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from canvas_sdk.commands.commands.questionnaire import QuestionnaireCommand
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ReviewOfSystemsCommand(QuestionnaireCommand):
|
|
5
|
+
"""A class for managing physical exam command."""
|
|
6
|
+
|
|
7
|
+
class Meta:
|
|
8
|
+
key = "ros"
|
|
9
|
+
commit_required_fields = ("questionnaire_id",)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from canvas_sdk.commands.commands.questionnaire import QuestionnaireCommand
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StructuredAssessmentCommand(QuestionnaireCommand):
|
|
5
|
+
"""A class for managing physical exam command."""
|
|
6
|
+
|
|
7
|
+
class Meta:
|
|
8
|
+
key = "structuredAssessment"
|
|
9
|
+
commit_required_fields = ("questionnaire_id",)
|
canvas_sdk/effects/__init__.py
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from canvas_sdk.effects import EffectType, _BaseEffect
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LaunchModalEffect(_BaseEffect):
|
|
8
|
+
"""An Effect that will launch a modal."""
|
|
9
|
+
|
|
10
|
+
class Meta:
|
|
11
|
+
effect_type = EffectType.LAUNCH_MODAL
|
|
12
|
+
|
|
13
|
+
class TargetType(StrEnum):
|
|
14
|
+
DEFAULT_MODAL = "default_modal"
|
|
15
|
+
NEW_WINDOW = "new_window"
|
|
16
|
+
RIGHT_CHART_PANE = "right_chart_pane"
|
|
17
|
+
|
|
18
|
+
url: str
|
|
19
|
+
target: TargetType = TargetType.DEFAULT_MODAL
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def values(self) -> dict[str, Any]:
|
|
23
|
+
"""The LaunchModalEffect values."""
|
|
24
|
+
return {"url": self.url, "target": self.target.value}
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from canvas_sdk.effects.base import EffectType, _BaseEffect
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IntakeFormResults(_BaseEffect):
|
|
9
|
+
"""An Effect that will decide which intake forms (questionnaires) appear on the patient portal."""
|
|
10
|
+
|
|
11
|
+
class Meta:
|
|
12
|
+
effect_type = EffectType.PATIENT_PORTAL__INTAKE_FORM_RESULTS
|
|
13
|
+
|
|
14
|
+
questionnaire_ids: list = Field(min_length=0)
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def values(self) -> dict[str, Any]:
|
|
18
|
+
"""The IntakeFormResults's values."""
|
|
19
|
+
return {"questionnaire_ids": [str(q) for q in self.questionnaire_ids]}
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def effect_payload(self) -> dict[str, Any]:
|
|
23
|
+
"""The payload of the effect."""
|
|
24
|
+
return {"data": self.values}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from canvas_generated.messages.effects_pb2 import EffectType
|
|
6
|
+
from canvas_sdk.effects.base import _BaseEffect
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ShowButtonEffect(_BaseEffect):
|
|
10
|
+
"""
|
|
11
|
+
An Effect that will decide an action button's properties.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
class Meta:
|
|
15
|
+
effect_type = EffectType.SHOW_ACTION_BUTTON
|
|
16
|
+
|
|
17
|
+
key: str = Field(min_length=1)
|
|
18
|
+
title: str = Field(min_length=1)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def values(self) -> dict[str, Any]:
|
|
22
|
+
"""The ShowButtonEffect's values."""
|
|
23
|
+
return {"key": self.key, "title": self.title}
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def effect_payload(self) -> dict[str, Any]:
|
|
27
|
+
"""The payload of the effect."""
|
|
28
|
+
return {"data": self.values}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
|
|
4
|
+
from canvas_sdk.effects import Effect
|
|
5
|
+
from canvas_sdk.effects.show_button import ShowButtonEffect
|
|
6
|
+
from canvas_sdk.events import EventType
|
|
7
|
+
from canvas_sdk.handlers.base import BaseHandler
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ActionButton(BaseHandler):
|
|
11
|
+
"""Base class for action buttons."""
|
|
12
|
+
|
|
13
|
+
RESPONDS_TO = [
|
|
14
|
+
EventType.Name(EventType.SHOW_NOTE_HEADER_BUTTON),
|
|
15
|
+
EventType.Name(EventType.SHOW_NOTE_FOOTER_BUTTON),
|
|
16
|
+
EventType.Name(EventType.ACTION_BUTTON_CLICKED),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
class ButtonLocation(StrEnum):
|
|
20
|
+
NOTE_HEADER = "note_header"
|
|
21
|
+
NOTE_FOOTER = "note_footer"
|
|
22
|
+
|
|
23
|
+
BUTTON_TITLE: str = ""
|
|
24
|
+
BUTTON_KEY: str = ""
|
|
25
|
+
BUTTON_LOCATION: ButtonLocation | None = None
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def handle(self) -> list[Effect]:
|
|
29
|
+
"""Method to handle button click."""
|
|
30
|
+
raise NotImplementedError("Implement to handle button click")
|
|
31
|
+
|
|
32
|
+
def visible(self) -> bool:
|
|
33
|
+
"""Method to determine button visibility."""
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
def compute(self) -> list[Effect]:
|
|
37
|
+
"""Method to compute the effects."""
|
|
38
|
+
if self.BUTTON_LOCATION is None:
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
if self.event.type in (
|
|
42
|
+
EventType.SHOW_NOTE_HEADER_BUTTON,
|
|
43
|
+
EventType.SHOW_NOTE_FOOTER_BUTTON,
|
|
44
|
+
):
|
|
45
|
+
if self.context["location"].lower() == self.BUTTON_LOCATION.value and self.visible():
|
|
46
|
+
return [ShowButtonEffect(key=self.BUTTON_KEY, title=self.BUTTON_TITLE).apply()]
|
|
47
|
+
else:
|
|
48
|
+
return []
|
|
49
|
+
elif (
|
|
50
|
+
self.event.type == EventType.ACTION_BUTTON_CLICKED
|
|
51
|
+
and self.context["key"] == self.BUTTON_KEY
|
|
52
|
+
):
|
|
53
|
+
return self.handle()
|
|
54
|
+
|
|
55
|
+
return []
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from canvas_sdk.effects import Effect
|
|
4
|
+
from canvas_sdk.events import EventType
|
|
5
|
+
from canvas_sdk.handlers import BaseHandler
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Application(BaseHandler, ABC):
|
|
9
|
+
"""An embeddable application that can be registered to Canvas."""
|
|
10
|
+
|
|
11
|
+
RESPONDS_TO = [EventType.Name(EventType.APPLICATION__ON_OPEN)]
|
|
12
|
+
|
|
13
|
+
def compute(self) -> list[Effect]:
|
|
14
|
+
"""Handle the application events."""
|
|
15
|
+
match self.event.type:
|
|
16
|
+
case EventType.APPLICATION__ON_OPEN:
|
|
17
|
+
return [self.on_open()] if self.target == self.identifier else []
|
|
18
|
+
case _:
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def on_open(self) -> Effect:
|
|
23
|
+
"""Handle the application open event."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def identifier(self) -> str:
|
|
28
|
+
"""The application identifier."""
|
|
29
|
+
return f"{self.__class__.__module__}:{self.__class__.__qualname__}"
|
canvas_sdk/handlers/base.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import importlib.metadata
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
4
5
|
import deprecation
|
|
5
6
|
|
|
7
|
+
from canvas_sdk.effects import Effect
|
|
6
8
|
from canvas_sdk.events import Event
|
|
7
9
|
|
|
8
10
|
version = importlib.metadata.version("canvas")
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
class BaseHandler:
|
|
13
|
+
class BaseHandler(ABC):
|
|
12
14
|
"""The class that all handlers inherit from."""
|
|
13
15
|
|
|
14
16
|
secrets: dict[str, Any]
|
|
@@ -43,3 +45,8 @@ class BaseHandler:
|
|
|
43
45
|
def target(self) -> str:
|
|
44
46
|
"""The target id of the event."""
|
|
45
47
|
return self.event.target.id
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def compute(self) -> list[Effect]:
|
|
51
|
+
"""Compute the effects to be applied."""
|
|
52
|
+
pass
|
canvas_sdk/protocols/base.py
CHANGED
canvas_sdk/v1/data/__init__.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from .billing import BillingLineItem
|
|
2
2
|
from .condition import Condition, ConditionCoding
|
|
3
3
|
from .medication import Medication, MedicationCoding
|
|
4
|
+
from .organization import Organization
|
|
4
5
|
from .patient import Patient
|
|
6
|
+
from .practicelocation import PracticeLocation
|
|
5
7
|
from .staff import Staff
|
|
6
8
|
from .task import Task, TaskComment, TaskLabel
|
|
7
9
|
|
|
@@ -11,7 +13,9 @@ __all__ = (
|
|
|
11
13
|
"ConditionCoding",
|
|
12
14
|
"Medication",
|
|
13
15
|
"MedicationCoding",
|
|
16
|
+
"Organization",
|
|
14
17
|
"Patient",
|
|
18
|
+
"PracticeLocation",
|
|
15
19
|
"Staff",
|
|
16
20
|
"Task",
|
|
17
21
|
"TaskComment",
|
canvas_sdk/v1/data/note.py
CHANGED
|
@@ -141,7 +141,7 @@ class Note(models.Model):
|
|
|
141
141
|
created = models.DateTimeField()
|
|
142
142
|
modified = models.DateTimeField()
|
|
143
143
|
patient = models.ForeignKey(Patient, on_delete=models.DO_NOTHING)
|
|
144
|
-
|
|
144
|
+
provider = models.ForeignKey("Staff", on_delete=models.DO_NOTHING, related_name="notes")
|
|
145
145
|
note_type = models.CharField(choices=NoteTypes.choices, null=True)
|
|
146
146
|
note_type_version = models.ForeignKey(
|
|
147
147
|
"NoteType", on_delete=models.DO_NOTHING, related_name="notes"
|
|
@@ -149,13 +149,12 @@ class Note(models.Model):
|
|
|
149
149
|
title = models.TextField()
|
|
150
150
|
body = models.JSONField()
|
|
151
151
|
originator = models.ForeignKey(CanvasUser, on_delete=models.DO_NOTHING)
|
|
152
|
-
|
|
152
|
+
last_modified_by_staff = models.ForeignKey("Staff", on_delete=models.DO_NOTHING, null=True)
|
|
153
153
|
checksum = models.CharField()
|
|
154
154
|
billing_note = models.TextField()
|
|
155
155
|
# TODO -implement InpatientStay model
|
|
156
156
|
# inpatient_stay = models.ForeignKey("InpatientStay", on_delete=models.DO_NOTHING)
|
|
157
157
|
related_data = models.JSONField()
|
|
158
|
-
|
|
159
|
-
# location = models.ForeignKey(PracticeLocation, on_delete=models.DO_NOTHING)
|
|
158
|
+
location = models.ForeignKey("PracticeLocation", on_delete=models.DO_NOTHING)
|
|
160
159
|
datetime_of_service = models.DateTimeField()
|
|
161
160
|
place_of_service = models.CharField()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
from canvas_sdk.v1.data.common import TaxIDType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Organization(models.Model):
|
|
7
|
+
"""Organization."""
|
|
8
|
+
|
|
9
|
+
class Meta:
|
|
10
|
+
managed = False
|
|
11
|
+
app_label = "canvas_sdk"
|
|
12
|
+
db_table = "canvas_sdk_data_api_organization_001"
|
|
13
|
+
|
|
14
|
+
dbid = models.BigIntegerField(primary_key=True)
|
|
15
|
+
created = models.DateTimeField()
|
|
16
|
+
modified = models.DateTimeField()
|
|
17
|
+
full_name = models.CharField()
|
|
18
|
+
short_name = models.CharField()
|
|
19
|
+
subdomain = models.CharField()
|
|
20
|
+
logo_url = models.CharField()
|
|
21
|
+
background_image_url = models.CharField()
|
|
22
|
+
background_gradient = models.CharField()
|
|
23
|
+
active = models.BooleanField()
|
|
24
|
+
tax_id = models.CharField(null=True)
|
|
25
|
+
tax_id_type = models.CharField(choices=TaxIDType.choices)
|
|
26
|
+
group_npi_number = models.CharField()
|
|
27
|
+
group_taxonomy_number = models.CharField()
|
|
28
|
+
include_zz_qualifier = models.BooleanField()
|
|
29
|
+
main_location = models.OneToOneField("PracticeLocation", on_delete=models.DO_NOTHING)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
from canvas_sdk.v1.data.common import TaxIDType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PracticeLocationPOS(models.TextChoices):
|
|
7
|
+
"""PracticeLocationPOS choices."""
|
|
8
|
+
|
|
9
|
+
PHARMACY = "01", "Pharmacy"
|
|
10
|
+
TELEHEALTH = "02", "Telehealth"
|
|
11
|
+
SCHOOL = "03", "Education Facility"
|
|
12
|
+
HOMELESS_SHELTER = "04", "Homeless Shelter"
|
|
13
|
+
PRISON = "09", "Prison"
|
|
14
|
+
TELEHEALTH_IN_PATIENT_HOME = "10", "Telehealth in Patient's Home"
|
|
15
|
+
OFFICE = "11", "Office"
|
|
16
|
+
HOME = "12", "Home"
|
|
17
|
+
ASSISTED_LIVING = "13", "Asssisted Living Facility"
|
|
18
|
+
GROUP_HOME = "14", "Group Home"
|
|
19
|
+
MOBILE = "15", "Mobile Unit"
|
|
20
|
+
WALK_IN_RETAIL = "17", "Walk-In Retail Health Clinic"
|
|
21
|
+
OFF_CAMPUS_OUTPATIENT_HOSPITAL = "19", "Off-Campus Outpatient Hospital"
|
|
22
|
+
URGENT_CARE = "20", "Urgent Care Facility"
|
|
23
|
+
INPATIENT_HOSPITAL = "21", "Inpatient Hospital"
|
|
24
|
+
ON_CAMPUS_OUTPATIENT_HOSPITAL = "22", "On-Campus Outpatient Hospital"
|
|
25
|
+
ER_HOSPITAL = "23", "Emergency Room Hospital"
|
|
26
|
+
AMBULATORY_SURGERY_CENTER = "24", "Ambulatory Surgery Center"
|
|
27
|
+
BIRTHING_CENTER = "25", "Birthing Center"
|
|
28
|
+
MILITARY_FACILITY = "26", "Military Treatment Facility"
|
|
29
|
+
STREET = "27", "Outreach Site / Street"
|
|
30
|
+
SNF = "31", "Skilled Nursing Facility"
|
|
31
|
+
NURSING = "32", "Nursing Facility"
|
|
32
|
+
CUSTODIAL = "33", "Custodial Care Facility"
|
|
33
|
+
HOSPICE = "34", "Hospice"
|
|
34
|
+
AMBULANCE_LAND = "41", "Ambulance Land"
|
|
35
|
+
AMBULANCE_AIR_WATER = "42", "Ambulance Air or Water"
|
|
36
|
+
INDEPENDENT_CLINIC = "49", "Independent Clinic"
|
|
37
|
+
FQHC = "50", "Federally Qualified Health Center"
|
|
38
|
+
PSYCH = "51", "Inpatient Psychiatric Facility"
|
|
39
|
+
PSYCH_PARTIAL = "52", "Inpatient Psychiatric Facility - Partial Hospitalization"
|
|
40
|
+
MENTAL_HEALTH_CENTER = "53", "Community Mental Health Center"
|
|
41
|
+
INTERMEDIATE_MENTAL = "54", "Intermediate Care Facility for Mentally Retarded"
|
|
42
|
+
SUBSTANCE_RESIDENTIAL = "55", "Residential Substance Abuse Treatment Facility"
|
|
43
|
+
PSYCH_RESIDENTIAL = "56", "Psychiatric Residential Treatment Center"
|
|
44
|
+
SUBSTANCE_NON_RESIDENTIAL = "57", "Non-Residential Substance Abuse Treatment Facility"
|
|
45
|
+
MASS_IMMUNIZATION = "60", "Mass Immunization Center"
|
|
46
|
+
INPATIENT_REHAB = "61", "Inpatient Rehabilitation Facility"
|
|
47
|
+
OUTPATIENT_REHAB = "62", "Outpatient Rehabilitation Facility"
|
|
48
|
+
ESRD = "65", "End-Stage Renal Disease Treatment Facility"
|
|
49
|
+
PUBLIC_CLINIC = "71", "State or Local Public Health Clinic"
|
|
50
|
+
RURAL_CLINIC = "72", "Rural Health Clinic"
|
|
51
|
+
INDEPENDENT_LAB = "81", "Independent Laboratory"
|
|
52
|
+
OTHER = "99", "Other Place of Service"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class PracticeLocation(models.Model):
|
|
56
|
+
"""PracticeLocation."""
|
|
57
|
+
|
|
58
|
+
class Meta:
|
|
59
|
+
managed = False
|
|
60
|
+
app_label = "canvas_sdk"
|
|
61
|
+
db_table = "canvas_sdk_data_api_practicelocation_001"
|
|
62
|
+
|
|
63
|
+
id = models.UUIDField()
|
|
64
|
+
dbid = models.BigIntegerField(primary_key=True)
|
|
65
|
+
created = models.DateTimeField()
|
|
66
|
+
modified = models.DateTimeField()
|
|
67
|
+
organization = models.ForeignKey(
|
|
68
|
+
"Organization", on_delete=models.DO_NOTHING, related_name="practice_locations"
|
|
69
|
+
)
|
|
70
|
+
place_of_service_code = models.CharField(choices=PracticeLocationPOS.choices)
|
|
71
|
+
full_name = models.CharField()
|
|
72
|
+
short_name = models.CharField()
|
|
73
|
+
background_image_url = models.CharField()
|
|
74
|
+
background_gradient = models.CharField()
|
|
75
|
+
active = models.BooleanField()
|
|
76
|
+
npi_number = models.CharField()
|
|
77
|
+
bill_through_organization = models.BooleanField()
|
|
78
|
+
tax_id = models.CharField()
|
|
79
|
+
tax_id_type = models.CharField(choices=TaxIDType.choices)
|
|
80
|
+
billing_location_name = models.CharField()
|
|
81
|
+
group_npi_number = models.CharField()
|
|
82
|
+
taxonomy_number = models.CharField()
|
|
83
|
+
include_zz_qualifier = models.BooleanField()
|
|
84
|
+
|
|
85
|
+
def __str__(self) -> str:
|
|
86
|
+
return self.full_name
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class PracticeLocationSetting(models.Model):
|
|
90
|
+
"""PracticeLocationSetting."""
|
|
91
|
+
|
|
92
|
+
class Meta:
|
|
93
|
+
managed = False
|
|
94
|
+
app_label = "canvas_sdk"
|
|
95
|
+
db_table = "canvas_sdk_data_api_practicelocationsetting_001"
|
|
96
|
+
|
|
97
|
+
dbid = models.BigIntegerField(primary_key=True)
|
|
98
|
+
practice_location = models.ForeignKey(
|
|
99
|
+
PracticeLocation, on_delete=models.DO_NOTHING, related_name="settings"
|
|
100
|
+
)
|
|
101
|
+
name = models.CharField()
|
|
102
|
+
value = models.JSONField()
|
|
103
|
+
|
|
104
|
+
def __str__(self) -> str:
|
|
105
|
+
return self.name
|
plugin_runner/plugin_runner.py
CHANGED
|
@@ -42,8 +42,8 @@ sys.path.append(PLUGIN_DIRECTORY)
|
|
|
42
42
|
# TODO: create typings here for the subkeys
|
|
43
43
|
LOADED_PLUGINS: dict = {}
|
|
44
44
|
|
|
45
|
-
# a global dictionary of events to
|
|
46
|
-
|
|
45
|
+
# a global dictionary of events to handler class names
|
|
46
|
+
EVENT_HANDLER_MAP: dict[str, list] = defaultdict(list)
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
class DataAccess(TypedDict):
|
|
@@ -63,6 +63,17 @@ Protocol = TypedDict(
|
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
ApplicationConfig = TypedDict(
|
|
67
|
+
"ApplicationConfig",
|
|
68
|
+
{
|
|
69
|
+
"class": str,
|
|
70
|
+
"description": str,
|
|
71
|
+
"icon": str,
|
|
72
|
+
"scope": str,
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
66
77
|
class Components(TypedDict):
|
|
67
78
|
"""Components."""
|
|
68
79
|
|
|
@@ -71,6 +82,7 @@ class Components(TypedDict):
|
|
|
71
82
|
content: list[dict]
|
|
72
83
|
effects: list[dict]
|
|
73
84
|
views: list[dict]
|
|
85
|
+
applications: list[ApplicationConfig]
|
|
74
86
|
|
|
75
87
|
|
|
76
88
|
class PluginManifest(TypedDict):
|
|
@@ -106,7 +118,7 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
106
118
|
event = Event(request)
|
|
107
119
|
event_type = event.type
|
|
108
120
|
event_name = event.name
|
|
109
|
-
relevant_plugins =
|
|
121
|
+
relevant_plugins = EVENT_HANDLER_MAP[event_name]
|
|
110
122
|
|
|
111
123
|
if event_type in [EventType.PLUGIN_CREATED, EventType.PLUGIN_UPDATED]:
|
|
112
124
|
plugin_name = event.target.id
|
|
@@ -117,22 +129,22 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
117
129
|
|
|
118
130
|
for plugin_name in relevant_plugins:
|
|
119
131
|
plugin = LOADED_PLUGINS[plugin_name]
|
|
120
|
-
|
|
132
|
+
handler_class = plugin["class"]
|
|
121
133
|
base_plugin_name = plugin_name.split(":")[0]
|
|
122
134
|
|
|
123
135
|
secrets = plugin.get("secrets", {})
|
|
124
136
|
secrets["graphql_jwt"] = token_for_plugin(plugin_name=plugin_name, audience="home")
|
|
125
137
|
|
|
126
138
|
try:
|
|
127
|
-
|
|
139
|
+
handler = handler_class(event, secrets)
|
|
128
140
|
classname = (
|
|
129
|
-
|
|
130
|
-
if isinstance(
|
|
141
|
+
handler.__class__.__name__
|
|
142
|
+
if isinstance(handler, ClinicalQualityMeasure)
|
|
131
143
|
else None
|
|
132
144
|
)
|
|
133
145
|
|
|
134
146
|
compute_start_time = time.time()
|
|
135
|
-
_effects = await asyncio.get_running_loop().run_in_executor(None,
|
|
147
|
+
_effects = await asyncio.get_running_loop().run_in_executor(None, handler.compute)
|
|
136
148
|
effects = [
|
|
137
149
|
Effect(
|
|
138
150
|
type=effect.type,
|
|
@@ -165,7 +177,7 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
165
177
|
|
|
166
178
|
event_duration = get_duration_ms(event_start_time)
|
|
167
179
|
|
|
168
|
-
# Don't log anything if a
|
|
180
|
+
# Don't log anything if a plugin handler didn't actually run.
|
|
169
181
|
if relevant_plugins:
|
|
170
182
|
log.info(f"Responded to Event {event_name} ({event_duration} ms)")
|
|
171
183
|
statsd_tags = tags_to_line_protocol({"event": event_name})
|
|
@@ -275,7 +287,7 @@ def sandbox_from_module(package_path: pathlib.Path, module_name: str) -> Any:
|
|
|
275
287
|
|
|
276
288
|
full_module_name = f"{package_path.name}.{module_name}"
|
|
277
289
|
|
|
278
|
-
sandbox = Sandbox(source_code,
|
|
290
|
+
sandbox = Sandbox(source_code, namespace=full_module_name)
|
|
279
291
|
return sandbox.execute()
|
|
280
292
|
|
|
281
293
|
|
|
@@ -306,43 +318,45 @@ def load_or_reload_plugin(path: pathlib.Path) -> None:
|
|
|
306
318
|
|
|
307
319
|
# TODO add existing schema validation from Michela here
|
|
308
320
|
try:
|
|
309
|
-
|
|
321
|
+
handlers = manifest_json["components"].get("protocols", []) + manifest_json[
|
|
322
|
+
"components"
|
|
323
|
+
].get("applications", [])
|
|
310
324
|
results = sandbox_from_package(path)
|
|
311
325
|
except Exception as e:
|
|
312
326
|
log.error(f'Unable to load plugin "{name}": {str(e)}')
|
|
313
327
|
return
|
|
314
328
|
|
|
315
|
-
for
|
|
329
|
+
for handler in handlers:
|
|
316
330
|
# TODO add class colon validation to existing schema validation
|
|
317
331
|
# TODO when we encounter an exception here, disable the plugin in response
|
|
318
332
|
try:
|
|
319
|
-
|
|
320
|
-
name_and_class = f"{name}:{
|
|
333
|
+
handler_module, handler_class = handler["class"].split(":")
|
|
334
|
+
name_and_class = f"{name}:{handler_module}:{handler_class}"
|
|
321
335
|
except ValueError:
|
|
322
|
-
log.error(f"Unable to parse class for plugin '{name}': '{
|
|
336
|
+
log.error(f"Unable to parse class for plugin '{name}': '{handler['class']}'")
|
|
323
337
|
continue
|
|
324
338
|
|
|
325
339
|
try:
|
|
326
340
|
if name_and_class in LOADED_PLUGINS:
|
|
327
341
|
log.info(f"Reloading plugin '{name_and_class}'")
|
|
328
342
|
|
|
329
|
-
result = results[
|
|
343
|
+
result = results[handler_module]
|
|
330
344
|
|
|
331
345
|
LOADED_PLUGINS[name_and_class]["active"] = True
|
|
332
346
|
|
|
333
|
-
LOADED_PLUGINS[name_and_class]["class"] = result[
|
|
347
|
+
LOADED_PLUGINS[name_and_class]["class"] = result[handler_class]
|
|
334
348
|
LOADED_PLUGINS[name_and_class]["sandbox"] = result
|
|
335
349
|
LOADED_PLUGINS[name_and_class]["secrets"] = secrets_json
|
|
336
350
|
else:
|
|
337
351
|
log.info(f"Loading plugin '{name_and_class}'")
|
|
338
352
|
|
|
339
|
-
result = results[
|
|
353
|
+
result = results[handler_module]
|
|
340
354
|
|
|
341
355
|
LOADED_PLUGINS[name_and_class] = {
|
|
342
356
|
"active": True,
|
|
343
|
-
"class": result[
|
|
357
|
+
"class": result[handler_class],
|
|
344
358
|
"sandbox": result,
|
|
345
|
-
"
|
|
359
|
+
"handler": handler,
|
|
346
360
|
"secrets": secrets_json,
|
|
347
361
|
}
|
|
348
362
|
except Exception as err:
|
|
@@ -353,17 +367,17 @@ def load_or_reload_plugin(path: pathlib.Path) -> None:
|
|
|
353
367
|
|
|
354
368
|
def refresh_event_type_map() -> None:
|
|
355
369
|
"""Ensure the event subscriptions are up to date."""
|
|
356
|
-
|
|
370
|
+
EVENT_HANDLER_MAP.clear()
|
|
357
371
|
|
|
358
372
|
for name, plugin in LOADED_PLUGINS.items():
|
|
359
373
|
if hasattr(plugin["class"], "RESPONDS_TO"):
|
|
360
374
|
responds_to = plugin["class"].RESPONDS_TO
|
|
361
375
|
|
|
362
376
|
if isinstance(responds_to, str):
|
|
363
|
-
|
|
377
|
+
EVENT_HANDLER_MAP[responds_to].append(name)
|
|
364
378
|
elif isinstance(responds_to, list):
|
|
365
379
|
for event in responds_to:
|
|
366
|
-
|
|
380
|
+
EVENT_HANDLER_MAP[event].append(name)
|
|
367
381
|
else:
|
|
368
382
|
log.warning(f"Unknown RESPONDS_TO type: {type(responds_to)}")
|
|
369
383
|
|