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.

Files changed (38) hide show
  1. {canvas-0.11.1.dist-info → canvas-0.13.0.dist-info}/METADATA +1 -1
  2. {canvas-0.11.1.dist-info → canvas-0.13.0.dist-info}/RECORD +38 -20
  3. canvas_cli/apps/plugin/plugin.py +13 -4
  4. canvas_cli/templates/plugins/application/cookiecutter.json +4 -0
  5. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json +28 -0
  6. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/README.md +11 -0
  7. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/applications/__init__.py +0 -0
  8. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/applications/my_application.py +12 -0
  9. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/assets/python-logo.png +0 -0
  10. canvas_cli/utils/validators/manifest_schema.py +18 -1
  11. canvas_generated/messages/effects_pb2.py +2 -2
  12. canvas_generated/messages/effects_pb2.pyi +36 -0
  13. canvas_generated/messages/events_pb2.py +2 -2
  14. canvas_generated/messages/events_pb2.pyi +10 -0
  15. canvas_sdk/commands/__init__.py +6 -0
  16. canvas_sdk/commands/commands/exam.py +9 -0
  17. canvas_sdk/commands/commands/review_of_systems.py +9 -0
  18. canvas_sdk/commands/commands/structured_assessment.py +9 -0
  19. canvas_sdk/effects/__init__.py +3 -1
  20. canvas_sdk/effects/launch_modal.py +24 -0
  21. canvas_sdk/effects/patient_portal/__init__.py +0 -0
  22. canvas_sdk/effects/patient_portal/intake_form_results.py +24 -0
  23. canvas_sdk/effects/show_button.py +28 -0
  24. canvas_sdk/handlers/action_button.py +55 -0
  25. canvas_sdk/handlers/application.py +29 -0
  26. canvas_sdk/handlers/base.py +8 -1
  27. canvas_sdk/protocols/base.py +3 -1
  28. canvas_sdk/v1/data/__init__.py +4 -0
  29. canvas_sdk/v1/data/note.py +3 -4
  30. canvas_sdk/v1/data/organization.py +29 -0
  31. canvas_sdk/v1/data/practicelocation.py +105 -0
  32. plugin_runner/plugin_runner.py +37 -23
  33. plugin_runner/sandbox.py +2 -6
  34. plugin_runner/tests/test_application.py +65 -0
  35. plugin_runner/tests/test_plugin_runner.py +4 -4
  36. plugin_runner/tests/test_sandbox.py +2 -2
  37. {canvas-0.11.1.dist-info → canvas-0.13.0.dist-info}/WHEEL +0 -0
  38. {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")
@@ -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 PhysicalExamCommand(QuestionnaireCommand):
5
+ """A class for managing physical exam command."""
6
+
7
+ class Meta:
8
+ key = "exam"
9
+ commit_required_fields = ("questionnaire_id",)
@@ -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",)
@@ -1,3 +1,5 @@
1
1
  from canvas_generated.messages.effects_pb2 import Effect, EffectType
2
2
 
3
- __all__ = ("Effect", "EffectType")
3
+ from .base import _BaseEffect
4
+
5
+ __all__ = ("Effect", "EffectType", "_BaseEffect")
@@ -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__}"
@@ -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
@@ -1,7 +1,9 @@
1
+ from abc import ABC
2
+
1
3
  from canvas_sdk.handlers.base import BaseHandler
2
4
 
3
5
 
4
- class BaseProtocol(BaseHandler):
6
+ class BaseProtocol(BaseHandler, ABC):
5
7
  """
6
8
  The class that protocols inherit from.
7
9
  """
@@ -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",
@@ -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
- # provider = models.ForeignKey(Staff, on_delete=models.DO_NOTHING, related_name="notes")
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
- # last_modified_by_staff = models.ForeignKey(Staff, on_delete=models.DO_NOTHING, null=True)
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
- # TODO -implement PracticeLocation model
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
@@ -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 protocol class names
46
- EVENT_PROTOCOL_MAP: dict[str, list] = defaultdict(list)
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 = EVENT_PROTOCOL_MAP[event_name]
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
- protocol_class = plugin["class"]
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
- protocol = protocol_class(event, secrets)
139
+ handler = handler_class(event, secrets)
128
140
  classname = (
129
- protocol.__class__.__name__
130
- if isinstance(protocol, ClinicalQualityMeasure)
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, protocol.compute)
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 protocol didn't actually run.
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, module_name=full_module_name)
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
- protocols = manifest_json["components"]["protocols"]
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 protocol in protocols:
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
- protocol_module, protocol_class = protocol["class"].split(":")
320
- name_and_class = f"{name}:{protocol_module}:{protocol_class}"
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}': '{protocol['class']}'")
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[protocol_module]
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[protocol_class]
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[protocol_module]
353
+ result = results[handler_module]
340
354
 
341
355
  LOADED_PLUGINS[name_and_class] = {
342
356
  "active": True,
343
- "class": result[protocol_class],
357
+ "class": result[handler_class],
344
358
  "sandbox": result,
345
- "protocol": protocol,
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
- EVENT_PROTOCOL_MAP.clear()
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
- EVENT_PROTOCOL_MAP[responds_to].append(name)
377
+ EVENT_HANDLER_MAP[responds_to].append(name)
364
378
  elif isinstance(responds_to, list):
365
379
  for event in responds_to:
366
- EVENT_PROTOCOL_MAP[event].append(name)
380
+ EVENT_HANDLER_MAP[event].append(name)
367
381
  else:
368
382
  log.warning(f"Unknown RESPONDS_TO type: {type(responds_to)}")
369
383