canvas 0.45.0__py3-none-any.whl → 0.47.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 (62) hide show
  1. {canvas-0.45.0.dist-info → canvas-0.47.0.dist-info}/METADATA +3 -2
  2. {canvas-0.45.0.dist-info → canvas-0.47.0.dist-info}/RECORD +62 -57
  3. canvas_generated/messages/effects_pb2.py +2 -2
  4. canvas_generated/messages/effects_pb2.pyi +10 -0
  5. canvas_generated/messages/events_pb2.py +2 -2
  6. canvas_generated/messages/events_pb2.pyi +18 -0
  7. canvas_sdk/commands/commands/exam.py +2 -1
  8. canvas_sdk/commands/commands/immunization_statement.py +32 -0
  9. canvas_sdk/commands/commands/questionnaire/__init__.py +18 -3
  10. canvas_sdk/commands/commands/questionnaire/question.py +3 -2
  11. canvas_sdk/commands/commands/questionnaire/toggle_questions.py +68 -0
  12. canvas_sdk/commands/commands/review_of_systems.py +2 -1
  13. canvas_sdk/v1/data/__init__.py +17 -3
  14. canvas_sdk/v1/data/allergy_intolerance.py +16 -19
  15. canvas_sdk/v1/data/appointment.py +10 -14
  16. canvas_sdk/v1/data/assessment.py +9 -10
  17. canvas_sdk/v1/data/banner_alert.py +12 -12
  18. canvas_sdk/v1/data/base.py +45 -1
  19. canvas_sdk/v1/data/billing.py +13 -18
  20. canvas_sdk/v1/data/business_line.py +7 -8
  21. canvas_sdk/v1/data/care_team.py +14 -17
  22. canvas_sdk/v1/data/charge_description_master.py +29 -0
  23. canvas_sdk/v1/data/claim.py +87 -95
  24. canvas_sdk/v1/data/claim_line_item.py +17 -18
  25. canvas_sdk/v1/data/command.py +8 -9
  26. canvas_sdk/v1/data/condition.py +9 -12
  27. canvas_sdk/v1/data/coverage.py +47 -53
  28. canvas_sdk/v1/data/detected_issue.py +16 -20
  29. canvas_sdk/v1/data/device.py +20 -21
  30. canvas_sdk/v1/data/discount.py +8 -8
  31. canvas_sdk/v1/data/imaging.py +24 -30
  32. canvas_sdk/v1/data/invoice.py +3 -3
  33. canvas_sdk/v1/data/lab.py +65 -84
  34. canvas_sdk/v1/data/line_item_transaction.py +7 -9
  35. canvas_sdk/v1/data/medication.py +14 -17
  36. canvas_sdk/v1/data/message.py +10 -17
  37. canvas_sdk/v1/data/note.py +27 -36
  38. canvas_sdk/v1/data/observation.py +24 -33
  39. canvas_sdk/v1/data/organization.py +14 -15
  40. canvas_sdk/v1/data/patient.py +57 -68
  41. canvas_sdk/v1/data/patient_consent.py +14 -19
  42. canvas_sdk/v1/data/payment_collection.py +7 -8
  43. canvas_sdk/v1/data/payor_specific_charge.py +10 -12
  44. canvas_sdk/v1/data/posting.py +10 -18
  45. canvas_sdk/v1/data/practicelocation.py +17 -21
  46. canvas_sdk/v1/data/protocol_override.py +8 -10
  47. canvas_sdk/v1/data/questionnaire.py +56 -73
  48. canvas_sdk/v1/data/reason_for_visit.py +7 -9
  49. canvas_sdk/v1/data/staff.py +61 -57
  50. canvas_sdk/v1/data/task.py +21 -31
  51. canvas_sdk/v1/data/team.py +15 -18
  52. canvas_sdk/v1/data/user.py +3 -3
  53. canvas_sdk/v1/data/utils.py +6 -0
  54. plugin_runner/allowed-module-imports.json +1340 -0
  55. plugin_runner/generate_allowed_imports.py +97 -0
  56. plugin_runner/plugin_runner.py +9 -0
  57. plugin_runner/sandbox.py +51 -60
  58. protobufs/canvas_generated/messages/effects.proto +6 -0
  59. protobufs/canvas_generated/messages/events.proto +12 -1
  60. settings.py +56 -22
  61. {canvas-0.45.0.dist-info → canvas-0.47.0.dist-info}/WHEEL +0 -0
  62. {canvas-0.45.0.dist-info → canvas-0.47.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,97 @@
1
+ import importlib
2
+ import json
3
+ import pkgutil
4
+ from collections.abc import Iterable
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ CANVAS_TOP_LEVEL_MODULES = (
9
+ "canvas_sdk.caching",
10
+ "canvas_sdk.commands",
11
+ "canvas_sdk.effects",
12
+ "canvas_sdk.events",
13
+ "canvas_sdk.handlers",
14
+ "canvas_sdk.protocols",
15
+ "canvas_sdk.questionnaires",
16
+ "canvas_sdk.templates",
17
+ "canvas_sdk.utils",
18
+ "canvas_sdk.v1",
19
+ "canvas_sdk.value_set",
20
+ "canvas_sdk.views",
21
+ "logger",
22
+ )
23
+
24
+
25
+ def find_submodules(starting_modules: Iterable[str]) -> list[str]:
26
+ """
27
+ Given a list of modules, return a list of those modules and their submodules.
28
+ """
29
+ submodules = set(starting_modules)
30
+
31
+ for module_path in starting_modules:
32
+ try:
33
+ module = importlib.import_module(module_path)
34
+
35
+ if not hasattr(module, "__path__"):
36
+ continue
37
+
38
+ for _, name, _ in pkgutil.walk_packages(module.__path__, prefix=module.__name__ + "."):
39
+ submodules.add(name)
40
+
41
+ except Exception as e:
42
+ print(f"could not import {module_path}: {e}")
43
+
44
+ return sorted(submodules)
45
+
46
+
47
+ def main() -> None:
48
+ """
49
+ Generate a JSON file of the allowed canvas_sdk imports.
50
+ """
51
+ print("Generating allowed canavs_sdk imports...")
52
+
53
+ CANVAS_SUBMODULE_NAMES = [
54
+ found_module
55
+ for found_module in find_submodules(CANVAS_TOP_LEVEL_MODULES)
56
+ # tests are excluded from the built and distributed module in pyproject.toml
57
+ if "tests" not in found_module and "test_" not in found_module
58
+ ]
59
+
60
+ CANVAS_MODULES: dict[str, set[str]] = {}
61
+
62
+ for module_name in CANVAS_SUBMODULE_NAMES:
63
+ module = importlib.import_module(module_name)
64
+
65
+ exports = getattr(module, "__exports__", None)
66
+
67
+ if not exports:
68
+ continue
69
+
70
+ if module_name not in CANVAS_MODULES:
71
+ CANVAS_MODULES[module_name] = set()
72
+
73
+ CANVAS_MODULES[module_name].update(exports)
74
+
75
+ # In use by a current plugin...
76
+ CANVAS_MODULES["canvas_sdk.commands"].add("*")
77
+
78
+ def default(o: Any) -> Any:
79
+ if isinstance(o, set):
80
+ return sorted(o)
81
+
82
+ raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
83
+
84
+ allowed_module_imports_path = Path(__file__).parent / "allowed-module-imports.json"
85
+
86
+ allowed_module_imports_path.write_text(
87
+ json.dumps(
88
+ CANVAS_MODULES,
89
+ default=default,
90
+ indent=2,
91
+ sort_keys=True,
92
+ )
93
+ )
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
@@ -53,11 +53,20 @@ from settings import (
53
53
  )
54
54
 
55
55
  if SENTRY_DSN:
56
+ # Lazy import for faster reload time in dev
57
+ from sentry_sdk.integrations.executing import ExecutingIntegration
58
+ from sentry_sdk.integrations.pure_eval import PureEvalIntegration
59
+
56
60
  sentry_sdk.init(
57
61
  dsn=SENTRY_DSN,
58
62
  environment=ENV,
63
+ integrations=[
64
+ ExecutingIntegration(),
65
+ PureEvalIntegration(),
66
+ ],
59
67
  release=os.getenv("CANVAS_PLUGINS_REPO_VERSION", "unknown"),
60
68
  send_default_pii=True,
69
+ spotlight=False,
61
70
  traces_sample_rate=0.0,
62
71
  profiles_sample_rate=0.0,
63
72
  )
plugin_runner/sandbox.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import ast
4
4
  import builtins
5
5
  import importlib
6
- import pkgutil
6
+ import json
7
7
  import sys
8
8
  import types
9
9
  from _ast import AnnAssign
@@ -43,25 +43,17 @@ if TYPE_CHECKING:
43
43
  names_to_module: dict[str, str]
44
44
 
45
45
 
46
- def find_submodules(starting_modules: Iterable[str]) -> list[str]:
47
- """
48
- Given a list of modules, return a list of those modules and their submodules.
49
- """
50
- submodules = set(starting_modules)
51
-
52
- for module_path in starting_modules:
53
- try:
54
- module = importlib.import_module(module_path)
46
+ try:
47
+ allowed_module_imports_path = Path(__file__).parent / "allowed-module-imports.json"
55
48
 
56
- if not hasattr(module, "__path__"):
57
- continue
49
+ CANVAS_MODULES: dict[str, Iterable[str]] = json.loads(allowed_module_imports_path.read_text())
58
50
 
59
- for _, name, _ in pkgutil.walk_packages(module.__path__, prefix=module.__name__ + "."):
60
- submodules.add(name)
61
- except Exception as e:
62
- print(f"could not import {module_path}: {e}")
51
+ for key in CANVAS_MODULES:
52
+ CANVAS_MODULES[key] = set(CANVAS_MODULES[key])
63
53
 
64
- return sorted(submodules)
54
+ except FileNotFoundError:
55
+ print("Error: Unable to load plugin_runner/allowed-module-imports.json, aborting")
56
+ sys.exit(1)
65
57
 
66
58
 
67
59
  SAFE_INTERNAL_DUNDER_READ_ATTRIBUTES = {
@@ -80,48 +72,6 @@ SAFE_EXTERNAL_DUNDER_READ_ATTRIBUTES = {
80
72
  "__name__",
81
73
  }
82
74
 
83
- CANVAS_TOP_LEVEL_MODULES = (
84
- "canvas_sdk.caching",
85
- "canvas_sdk.commands",
86
- "canvas_sdk.effects",
87
- "canvas_sdk.events",
88
- "canvas_sdk.handlers",
89
- "canvas_sdk.protocols",
90
- "canvas_sdk.questionnaires",
91
- "canvas_sdk.templates",
92
- "canvas_sdk.utils",
93
- "canvas_sdk.v1",
94
- "canvas_sdk.value_set",
95
- "canvas_sdk.views",
96
- "logger",
97
- )
98
-
99
- CANVAS_SUBMODULE_NAMES = [
100
- found_module
101
- for found_module in find_submodules(CANVAS_TOP_LEVEL_MODULES)
102
- # tests are excluded from the built and distributed module in pyproject.toml
103
- if "tests" not in found_module and "test_" not in found_module
104
- ]
105
-
106
- CANVAS_MODULES: dict[str, set[str]] = {}
107
-
108
- for module_name in CANVAS_SUBMODULE_NAMES:
109
- module = importlib.import_module(module_name)
110
-
111
- exports = getattr(module, "__exports__", None)
112
-
113
- if not exports:
114
- continue
115
-
116
- if module_name not in CANVAS_MODULES:
117
- CANVAS_MODULES[module_name] = set()
118
-
119
- CANVAS_MODULES[module_name].update(exports)
120
-
121
- # In use by a current plugin...
122
- CANVAS_MODULES["canvas_sdk.commands"].add("*")
123
-
124
-
125
75
  STANDARD_LIBRARY_MODULES = {
126
76
  "__future__": {
127
77
  "annotations",
@@ -211,6 +161,7 @@ STANDARD_LIBRARY_MODULES = {
211
161
  "Literal",
212
162
  "NamedTuple",
213
163
  "NotRequired",
164
+ "Optional",
214
165
  "Protocol",
215
166
  "Sequence",
216
167
  "Tuple",
@@ -397,7 +348,47 @@ class Sandbox:
397
348
 
398
349
  def visit_AnnAssign(self, node: AnnAssign) -> AnnAssign:
399
350
  """Allow type annotations."""
400
- return node
351
+ return self.node_contents_visit(node)
352
+
353
+ def visit_Match(self, node: ast.Match) -> ast.Match:
354
+ """Allow `match`."""
355
+ return self.node_contents_visit(node)
356
+
357
+ def visit_MatchAs(self, node: ast.MatchAs) -> ast.MatchAs:
358
+ """Allow `match`."""
359
+ return self.node_contents_visit(node)
360
+
361
+ def visit_MatchClass(self, node: ast.MatchClass) -> ast.MatchClass:
362
+ """Allow `match`."""
363
+ return self.node_contents_visit(node)
364
+
365
+ def visit_MatchMapping(self, node: ast.MatchMapping) -> ast.MatchMapping:
366
+ """Allow `match`."""
367
+ return self.node_contents_visit(node)
368
+
369
+ def visit_MatchOr(self, node: ast.MatchOr) -> ast.MatchOr:
370
+ """Allow `match`."""
371
+ return self.node_contents_visit(node)
372
+
373
+ def visit_MatchSingleton(self, node: ast.MatchSingleton) -> ast.MatchSingleton:
374
+ """Allow `match`."""
375
+ return self.node_contents_visit(node)
376
+
377
+ def visit_MatchSequence(self, node: ast.MatchSequence) -> ast.MatchSequence:
378
+ """Allow `match`."""
379
+ return self.node_contents_visit(node)
380
+
381
+ def visit_MatchStar(self, node: ast.MatchStar) -> ast.MatchStar:
382
+ """Allow `match`."""
383
+ return self.node_contents_visit(node)
384
+
385
+ def visit_MatchValue(self, node: ast.MatchValue) -> ast.MatchValue:
386
+ """Allow `match`."""
387
+ return self.node_contents_visit(node)
388
+
389
+ def visit_match_case(self, node: ast.match_case) -> ast.match_case:
390
+ """Allow `match`."""
391
+ return self.node_contents_visit(node)
401
392
 
402
393
  def check_import_names(self, node: ast.ImportFrom) -> ast.AST:
403
394
  """Check the names being imported.
@@ -235,6 +235,12 @@ enum EffectType {
235
235
 
236
236
  ORIGINATE_CHART_SECTION_REVIEW_COMMAND = 1400;
237
237
 
238
+ ORIGINATE_IMMUNIZATION_STATEMENT_COMMAND = 1500;
239
+ EDIT_IMMUNIZATION_STATEMENT_COMMAND = 1501;
240
+ DELETE_IMMUNIZATION_STATEMENT_COMMAND = 1502;
241
+ COMMIT_IMMUNIZATION_STATEMENT_COMMAND = 1503;
242
+ ENTER_IN_ERROR_IMMUNIZATION_STATEMENT_COMMAND = 1504;
243
+
238
244
  SHOW_ACTION_BUTTON = 1000;
239
245
 
240
246
  PATIENT_PORTAL__FORM_RESULT = 2000;
@@ -54,6 +54,8 @@ enum EventType {
54
54
  PRESCRIPTION_UPDATED = 46;
55
55
  REFERRAL_REPORT_CREATED = 47;
56
56
  REFERRAL_REPORT_UPDATED = 48;
57
+ STAFF_CREATED = 49;
58
+ STAFF_UPDATED = 50;
57
59
  TASK_COMMENT_CREATED = 51;
58
60
  TASK_CREATED = 54;
59
61
  TASK_LABELS_ADJUSTED = 55;
@@ -98,6 +100,9 @@ enum EventType {
98
100
  DETECTED_ISSUE_EVIDENCE_CREATED = 88;
99
101
  DETECTED_ISSUE_EVIDENCE_UPDATED = 89;
100
102
 
103
+ STAFF_ACTIVATED = 90;
104
+ STAFF_DEACTIVATED = 91;
105
+
101
106
  // General Command Events
102
107
 
103
108
  PRE_COMMAND_ORIGINATE = 100;
@@ -1096,10 +1101,16 @@ enum EventType {
1096
1101
  SIMPLE_API_WEBSOCKET_AUTHENTICATE = 130002;
1097
1102
 
1098
1103
  PATIENT_METADATA__GET_ADDITIONAL_FIELDS = 140000;
1099
-
1104
+
1100
1105
  PATIENT_EXTERNAL_IDENTIFIER_CREATED = 140001;
1101
1106
  PATIENT_EXTERNAL_IDENTIFIER_UPDATED = 140002;
1102
1107
  PATIENT_EXTERNAL_IDENTIFIER_DELETED = 140003;
1108
+ PATIENT_METADATA_CREATED = 140004;
1109
+ PATIENT_METADATA_UPDATED = 140005;
1110
+
1111
+ DOCUMENT_REFERENCE_CREATED = 150000;
1112
+ DOCUMENT_REFERENCE_UPDATED = 150001;
1113
+ DOCUMENT_REFERENCE_DELETED = 150002;
1103
1114
  }
1104
1115
 
1105
1116
  message Event {
settings.py CHANGED
@@ -2,14 +2,17 @@ import logging
2
2
  import os
3
3
  import sys
4
4
  from pathlib import Path
5
+ from typing import Any
5
6
  from urllib import parse
6
7
 
8
+ from django.core.exceptions import ImproperlyConfigured
7
9
  from dotenv import load_dotenv
8
10
  from env_tools import env_to_bool
9
11
 
10
12
  load_dotenv()
11
13
 
12
14
  BASE_DIR = Path(__file__).resolve().parent.resolve()
15
+
13
16
  FOURTEEN_DAYS = 60 * 60 * 24 * 14
14
17
 
15
18
  ENV = os.getenv("ENV", "development")
@@ -62,35 +65,66 @@ CANVAS_SDK_DB_USERNAME = os.getenv("CANVAS_SDK_DB_USERNAME", "app")
62
65
  CANVAS_SDK_DB_PASSWORD = os.getenv("CANVAS_SDK_DB_PASSWORD", "app")
63
66
  CANVAS_SDK_DB_HOST = os.getenv("CANVAS_SDK_DB_HOST", "home-app-db")
64
67
  CANVAS_SDK_DB_PORT = os.getenv("CANVAS_SDK_DB_PORT", "5432")
68
+ CANVAS_SDK_DB_URL = os.getenv("DATABASE_URL")
69
+
70
+ if IS_TESTING:
71
+ CANVAS_SDK_DB_BACKEND = "sqlite3"
72
+ elif CANVAS_SDK_DB_URL:
73
+ CANVAS_SDK_DB_BACKEND = "postgres"
74
+ else:
75
+ # Default to sqlite3 for local development if no DATABASE_URL is set
76
+ CANVAS_SDK_DB_BACKEND = "sqlite3" if IS_SCRIPT else "postgres"
65
77
 
66
78
  PLUGIN_RUNNER_MAX_WORKERS = int(os.getenv("PLUGIN_RUNNER_MAX_WORKERS", 5))
67
79
 
68
- if os.getenv("DATABASE_URL"):
69
- parsed_url = parse.urlparse(os.getenv("DATABASE_URL"))
80
+ if CANVAS_SDK_DB_BACKEND == "postgres":
81
+ db_config: dict[str, Any] = {
82
+ "ENGINE": "django.db.backends.postgresql",
83
+ "OPTIONS": {"pool": {"min_size": 2, "max_size": PLUGIN_RUNNER_MAX_WORKERS}},
84
+ }
70
85
 
86
+ if CANVAS_SDK_DB_URL:
87
+ parsed_url = parse.urlparse(CANVAS_SDK_DB_URL)
88
+ db_config.update(
89
+ {
90
+ "NAME": parsed_url.path[1:],
91
+ "USER": os.getenv("CANVAS_SDK_DATABASE_ROLE"),
92
+ "PASSWORD": os.getenv("CANVAS_SDK_DATABASE_ROLE_PASSWORD"),
93
+ "HOST": parsed_url.hostname,
94
+ "PORT": parsed_url.port,
95
+ }
96
+ )
97
+ else:
98
+ db_config.update(
99
+ {
100
+ "NAME": CANVAS_SDK_DB_NAME,
101
+ "USER": CANVAS_SDK_DB_USERNAME,
102
+ "PASSWORD": CANVAS_SDK_DB_PASSWORD,
103
+ "HOST": CANVAS_SDK_DB_HOST,
104
+ "PORT": CANVAS_SDK_DB_PORT,
105
+ }
106
+ )
107
+
108
+ DATABASES = {"default": db_config}
109
+
110
+ elif CANVAS_SDK_DB_BACKEND == "sqlite3":
71
111
  DATABASES = {
72
112
  "default": {
73
- "ENGINE": "django.db.backends.postgresql",
74
- "OPTIONS": {"pool": {"min_size": 2, "max_size": PLUGIN_RUNNER_MAX_WORKERS}},
75
- "NAME": parsed_url.path[1:],
76
- "USER": os.getenv("CANVAS_SDK_DATABASE_ROLE"),
77
- "PASSWORD": os.getenv("CANVAS_SDK_DATABASE_ROLE_PASSWORD"),
78
- "HOST": parsed_url.hostname,
79
- "PORT": parsed_url.port,
80
- }
113
+ "ENGINE": "django.db.backends.sqlite3",
114
+ "NAME": f"file:{str(BASE_DIR / 'db.sqlite3')}?mode=ro",
115
+ "OPTIONS": {"uri": True},
116
+ },
117
+ "default-write": {
118
+ "ENGINE": "django.db.backends.sqlite3",
119
+ "NAME": str(BASE_DIR / "db.sqlite3"),
120
+ },
81
121
  }
122
+
82
123
  else:
83
- DATABASES = {
84
- "default": {
85
- "ENGINE": "django.db.backends.postgresql",
86
- "OPTIONS": {"pool": {"min_size": 2, "max_size": PLUGIN_RUNNER_MAX_WORKERS}},
87
- "NAME": CANVAS_SDK_DB_NAME,
88
- "USER": CANVAS_SDK_DB_USERNAME,
89
- "PASSWORD": CANVAS_SDK_DB_PASSWORD,
90
- "HOST": CANVAS_SDK_DB_HOST,
91
- "PORT": CANVAS_SDK_DB_PORT,
92
- }
93
- }
124
+ raise ImproperlyConfigured(
125
+ "Unsupported database backend specified in 'CANVAS_SDK_DB_BACKEND' setting. "
126
+ "Supported values are 'postgres' and 'sqlite3'."
127
+ )
94
128
 
95
129
  AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID", "")
96
130
  AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY", "")
@@ -129,7 +163,7 @@ TEMPLATES = [
129
163
  ]
130
164
 
131
165
 
132
- if IS_TESTING:
166
+ if IS_SCRIPT:
133
167
  CACHES = {
134
168
  "default": {
135
169
  "BACKEND": "django.core.cache.backends.locmem.LocMemCache",