canvas 0.35.0__py3-none-any.whl → 0.36.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 (42) hide show
  1. {canvas-0.35.0.dist-info → canvas-0.36.0.dist-info}/METADATA +2 -2
  2. {canvas-0.35.0.dist-info → canvas-0.36.0.dist-info}/RECORD +42 -34
  3. canvas_cli/apps/plugin/plugin.py +3 -2
  4. canvas_cli/utils/validators/manifest_schema.py +15 -1
  5. canvas_generated/messages/effects_pb2.py +2 -2
  6. canvas_generated/messages/effects_pb2.pyi +8 -0
  7. canvas_generated/messages/events_pb2.py +2 -2
  8. canvas_generated/messages/events_pb2.pyi +14 -0
  9. canvas_sdk/caching/__init__.py +1 -0
  10. canvas_sdk/caching/base.py +127 -0
  11. canvas_sdk/caching/client.py +24 -0
  12. canvas_sdk/caching/exceptions.py +21 -0
  13. canvas_sdk/caching/plugins.py +20 -0
  14. canvas_sdk/caching/utils.py +28 -0
  15. canvas_sdk/commands/__init__.py +2 -0
  16. canvas_sdk/commands/commands/chart_section_review.py +23 -0
  17. canvas_sdk/effects/launch_modal.py +1 -0
  18. canvas_sdk/effects/simple_api.py +61 -2
  19. canvas_sdk/handlers/simple_api/websocket.py +79 -0
  20. canvas_sdk/value_set/custom.py +1 -1
  21. canvas_sdk/value_set/v2022/allergy.py +1 -1
  22. canvas_sdk/value_set/v2022/assessment.py +1 -1
  23. canvas_sdk/value_set/v2022/communication.py +1 -1
  24. canvas_sdk/value_set/v2022/condition.py +1 -1
  25. canvas_sdk/value_set/v2022/device.py +1 -1
  26. canvas_sdk/value_set/v2022/diagnostic_study.py +1 -1
  27. canvas_sdk/value_set/v2022/encounter.py +1 -1
  28. canvas_sdk/value_set/v2022/immunization.py +1 -1
  29. canvas_sdk/value_set/v2022/individual_characteristic.py +1 -1
  30. canvas_sdk/value_set/v2022/intervention.py +1 -1
  31. canvas_sdk/value_set/v2022/laboratory_test.py +1 -1
  32. canvas_sdk/value_set/v2022/medication.py +1 -1
  33. canvas_sdk/value_set/v2022/physical_exam.py +1 -1
  34. canvas_sdk/value_set/v2022/procedure.py +1 -1
  35. plugin_runner/plugin_runner.py +72 -90
  36. plugin_runner/sandbox.py +1 -0
  37. protobufs/canvas_generated/messages/effects.proto +6 -0
  38. protobufs/canvas_generated/messages/events.proto +8 -12
  39. pubsub/pubsub.py +10 -1
  40. settings.py +32 -0
  41. {canvas-0.35.0.dist-info → canvas-0.36.0.dist-info}/WHEEL +0 -0
  42. {canvas-0.35.0.dist-info → canvas-0.36.0.dist-info}/entry_points.txt +0 -0
@@ -1,23 +1,28 @@
1
- import asyncio
2
1
  import json
3
2
  import os
4
3
  import pathlib
5
4
  import pickle
6
5
  import pkgutil
7
6
  import sys
7
+ import threading
8
8
  import traceback
9
9
  from collections import defaultdict
10
- from collections.abc import AsyncGenerator
10
+ from collections.abc import Iterable
11
+ from concurrent.futures import ThreadPoolExecutor
11
12
  from http import HTTPStatus
13
+ from time import sleep
12
14
  from typing import Any, TypedDict
13
15
 
14
16
  import grpc
15
- import redis.asyncio as redis
17
+ import redis
16
18
  import sentry_sdk
17
- from asgiref.sync import sync_to_async
18
- from django.db import connections
19
+ from django.core.signals import request_finished, request_started
20
+ from redis.backoff import ExponentialBackoff
21
+ from redis.exceptions import ConnectionError, TimeoutError
22
+ from redis.retry import Retry
19
23
  from sentry_sdk.integrations.logging import ignore_logger
20
24
 
25
+ import settings
21
26
  from canvas_generated.messages.effects_pb2 import EffectType
22
27
  from canvas_generated.messages.plugins_pb2 import ReloadPluginsRequest, ReloadPluginsResponse
23
28
  from canvas_generated.services.plugin_runner_pb2_grpc import (
@@ -27,9 +32,10 @@ from canvas_generated.services.plugin_runner_pb2_grpc import (
27
32
  from canvas_sdk.effects import Effect
28
33
  from canvas_sdk.effects.simple_api import Response
29
34
  from canvas_sdk.events import Event, EventRequest, EventResponse, EventType
35
+ from canvas_sdk.handlers.simple_api.websocket import DenyConnection
30
36
  from canvas_sdk.protocols import ClinicalQualityMeasure
31
37
  from canvas_sdk.utils import metrics
32
- from canvas_sdk.utils.metrics import measured, statsd_client
38
+ from canvas_sdk.utils.metrics import measured
33
39
  from logger import log
34
40
  from plugin_runner.authentication import token_for_plugin
35
41
  from plugin_runner.installation import install_plugins
@@ -39,7 +45,6 @@ from settings import (
39
45
  CUSTOMER_IDENTIFIER,
40
46
  ENV,
41
47
  IS_PRODUCTION_CUSTOMER,
42
- IS_TESTING,
43
48
  MANIFEST_FILE_NAME,
44
49
  PLUGIN_DIRECTORY,
45
50
  REDIS_ENDPOINT,
@@ -149,33 +154,12 @@ class PluginManifest(TypedDict):
149
154
  readme: str
150
155
 
151
156
 
152
- @sync_to_async
153
- def reconnect_if_needed() -> None:
154
- """
155
- Reconnect to the database if the connection has been closed.
156
-
157
- NOTE: Django database functions are not async-safe and must be called
158
- via e.g. sync_to_async. They will silently fail and block the handler
159
- if called in an async context directly.
160
- """
161
- if IS_TESTING:
162
- return
163
-
164
- connection = connections["default"]
165
-
166
- if not connection.is_usable():
167
- log.debug("Connection was unusable, reconnecting...")
168
- connection.connect()
169
-
170
-
171
157
  class PluginRunner(PluginRunnerServicer):
172
158
  """This process runs provided plugins that register interest in incoming events."""
173
159
 
174
160
  sandbox: Sandbox
175
161
 
176
- async def HandleEvent(
177
- self, request: EventRequest, context: Any
178
- ) -> AsyncGenerator[EventResponse, None]:
162
+ def HandleEvent(self, request: EventRequest, context: Any) -> Iterable[EventResponse]:
179
163
  """This is invoked when an event comes in."""
180
164
  event = Event(request)
181
165
  with metrics.measure(
@@ -190,13 +174,18 @@ class PluginRunner(PluginRunnerServicer):
190
174
  sentry_sdk.set_tag("event-name", event_name)
191
175
 
192
176
  if relevant_plugins:
193
- await reconnect_if_needed()
177
+ # Send the Django request_started signal
178
+ request_started.send(sender=self.__class__)
194
179
 
195
180
  if event_type in [EventType.PLUGIN_CREATED, EventType.PLUGIN_UPDATED]:
196
181
  plugin_name = event.target.id
197
182
  # filter only for the plugin(s) that were created/updated
198
183
  relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
199
- elif event_type in {EventType.SIMPLE_API_AUTHENTICATE, EventType.SIMPLE_API_REQUEST}:
184
+ elif event_type in {
185
+ EventType.SIMPLE_API_AUTHENTICATE,
186
+ EventType.SIMPLE_API_REQUEST,
187
+ EventType.SIMPLE_API_WEBSOCKET_AUTHENTICATE,
188
+ }:
200
189
  # The target plugin's name will be part of the home-app URL path, so other plugins that
201
190
  # respond to SimpleAPI request events are not relevant
202
191
  plugin_name = event.context["plugin_name"]
@@ -238,7 +227,7 @@ class PluginRunner(PluginRunnerServicer):
238
227
  "event": event_name,
239
228
  },
240
229
  ):
241
- _effects = await sync_to_async(handler.compute)()
230
+ _effects = handler.compute()
242
231
  effects = [
243
232
  Effect(
244
233
  type=effect.type,
@@ -283,27 +272,42 @@ class PluginRunner(PluginRunnerServicer):
283
272
  f" {event.context['path']}"
284
273
  )
285
274
  effect_list = [Response(status_code=HTTPStatus.INTERNAL_SERVER_ERROR).apply()]
275
+ if event.type == EventType.SIMPLE_API_WEBSOCKET_AUTHENTICATE:
276
+ if len(relevant_plugin_handlers) == 0:
277
+ effect_list = [DenyConnection().apply()]
278
+ elif len(relevant_plugin_handlers) > 1:
279
+ log.error(
280
+ f"Multiple handlers responded to {EventType.Name(EventType.SIMPLE_API_WEBSOCKET_AUTHENTICATE)}"
281
+ f" {event.context['channel']}"
282
+ )
283
+ effect_list = [DenyConnection().apply()]
286
284
 
287
285
  # Don't log anything if a plugin handler didn't actually run.
288
286
  if relevant_plugins:
287
+ # Send the Django request_finished signal
288
+ request_finished.send(sender=self.__class__)
289
+
289
290
  log.info(f"Responded to Event {event_name}.")
290
291
 
291
292
  yield EventResponse(success=True, effects=effect_list)
292
293
 
293
- async def ReloadPlugins(
294
+ def ReloadPlugins(
294
295
  self, request: ReloadPluginsRequest, context: Any
295
- ) -> AsyncGenerator[ReloadPluginsResponse, None]:
296
+ ) -> Iterable[ReloadPluginsResponse]:
296
297
  """This is invoked when we need to reload plugins."""
297
298
  log.info("Reloading plugins...")
298
299
  try:
299
- await publish_message(message={"action": "reload"})
300
+ publish_message(message={"action": "reload"})
300
301
  except ImportError:
301
302
  yield ReloadPluginsResponse(success=False)
302
303
  else:
303
304
  yield ReloadPluginsResponse(success=True)
304
305
 
305
306
 
306
- async def synchronize_plugins(run_once: bool = False) -> None:
307
+ STOP_SYNCHRONIZER = threading.Event()
308
+
309
+
310
+ def synchronize_plugins(run_once: bool = False) -> None:
307
311
  """
308
312
  Listen for messages on the pubsub channel that will indicate it is
309
313
  necessary to reinstall and reload plugins.
@@ -312,16 +316,10 @@ async def synchronize_plugins(run_once: bool = False) -> None:
312
316
 
313
317
  _, pubsub = get_client()
314
318
 
315
- await pubsub.psubscribe(CHANNEL_NAME)
316
-
317
- while True:
318
- message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=5.0)
319
+ pubsub.psubscribe(CHANNEL_NAME)
319
320
 
320
- await pubsub.check_health()
321
-
322
- if not pubsub.connection.is_connected: # type: ignore
323
- log.info("synchronize_plugins: reconnecting to Redis")
324
- await pubsub.connection.connect() # type: ignore
321
+ while not STOP_SYNCHRONIZER.is_set():
322
+ message = pubsub.get_message(ignore_subscribe_messages=True, timeout=5.0)
325
323
 
326
324
  if message is None:
327
325
  continue
@@ -357,21 +355,21 @@ async def synchronize_plugins(run_once: bool = False) -> None:
357
355
  break
358
356
 
359
357
 
360
- async def synchronize_plugins_and_report_errors() -> None:
358
+ def synchronize_plugins_and_report_errors() -> None:
361
359
  """
362
360
  Run synchronize_plugins() in perpetuity and report any encountered errors.
363
361
  """
364
362
  log.info("synchronize_plugins: starting loop...")
365
363
 
366
- while True:
364
+ while not STOP_SYNCHRONIZER.is_set():
367
365
  try:
368
- await synchronize_plugins()
366
+ synchronize_plugins()
369
367
  except Exception as e:
370
368
  log.error(f"synchronize_plugins: error: {e}")
371
369
  sentry_sdk.capture_exception(e)
372
370
 
373
371
  # don't crush redis if we're retrying in a tight loop
374
- await asyncio.sleep(0.5)
372
+ sleep(0.5)
375
373
 
376
374
 
377
375
  def validate_effects(effects: list[Effect]) -> list[Effect]:
@@ -434,17 +432,22 @@ def find_modules(base_path: pathlib.Path, prefix: str | None = None) -> list[str
434
432
  return modules
435
433
 
436
434
 
437
- async def publish_message(message: dict) -> None:
435
+ def publish_message(message: dict) -> None:
438
436
  """Publish a message to the pubsub channel."""
439
437
  log.info(f'Publishing message to pubsub channel "{CHANNEL_NAME}"')
440
438
  client, _ = get_client()
441
439
 
442
- await client.publish(CHANNEL_NAME, pickle.dumps(message))
440
+ client.publish(CHANNEL_NAME, pickle.dumps(message))
443
441
 
444
442
 
445
443
  def get_client() -> tuple[redis.Redis, redis.client.PubSub]:
446
- """Return an async Redis client and pubsub object."""
447
- client = redis.Redis.from_url(REDIS_ENDPOINT)
444
+ """Return a Redis client and pubsub object."""
445
+ client = redis.from_url(
446
+ REDIS_ENDPOINT,
447
+ retry=Retry(backoff=ExponentialBackoff(), retries=10),
448
+ retry_on_error=[ConnectionError, TimeoutError, ConnectionResetError],
449
+ health_check_interval=1,
450
+ )
448
451
  pubsub = client.pubsub()
449
452
 
450
453
  return client, pubsub
@@ -594,62 +597,41 @@ def load_plugins(specified_plugin_paths: list[str] | None = None) -> None:
594
597
 
595
598
  refresh_event_type_map()
596
599
 
597
- for key in EventType.keys(): # noqa: SIM118
598
- value = len(EVENT_HANDLER_MAP[key]) if key in EVENT_HANDLER_MAP else 0
599
- statsd_client.gauge("plugins.event_handler_count", value, tags={"event": key})
600
600
 
601
-
602
- _cleanup_coroutines = []
603
-
604
-
605
- async def serve(specified_plugin_paths: list[str] | None = None) -> None:
606
- """Run the server."""
601
+ # NOTE: specified_plugin_paths powers the `canvas run-plugins` command
602
+ def main(specified_plugin_paths: list[str] | None = None) -> None:
603
+ """Run the server and the synchronize_plugins loop."""
607
604
  port = "50051"
608
605
 
609
- server = grpc.aio.server()
606
+ executor = ThreadPoolExecutor(max_workers=settings.PLUGIN_RUNNER_MAX_WORKERS)
607
+ server = grpc.server(thread_pool=executor)
610
608
  server.add_insecure_port("127.0.0.1:" + port)
611
609
 
612
610
  add_PluginRunnerServicer_to_server(PluginRunner(), server)
613
611
 
614
612
  log.info(f"Starting server, listening on port {port}")
615
613
 
616
- # Only install plugins if the plugin runner was not started from the CLI
614
+ # Only install plugins and start the synchronizer thread if the plugin runner was not started
615
+ # from the CLI
616
+ synchronizer_thread = threading.Thread(target=synchronize_plugins_and_report_errors)
617
617
  if specified_plugin_paths is None:
618
618
  install_plugins()
619
+ STOP_SYNCHRONIZER.clear()
620
+ synchronizer_thread.start()
619
621
 
620
622
  load_plugins(specified_plugin_paths)
621
623
 
622
- await server.start()
623
-
624
- async def server_graceful_shutdown() -> None:
625
- log.info("Starting graceful shutdown...")
626
- await server.stop(5)
627
-
628
- _cleanup_coroutines.append(server_graceful_shutdown())
629
-
630
- await server.wait_for_termination()
631
-
632
-
633
- # NOTE: specified_plugin_paths powers the `canvas run-plugins` command
634
- def main(specified_plugin_paths: list[str] | None = None) -> None:
635
- """Run the server and the synchronize_plugins loop."""
636
- loop = asyncio.new_event_loop()
637
-
638
- asyncio.set_event_loop(loop)
624
+ server.start()
639
625
 
640
626
  try:
641
- coroutines = [serve(specified_plugin_paths)]
642
-
643
- # Only start the synchronizer if the plugin runner was not started from the CLI
644
- if specified_plugin_paths is None:
645
- coroutines.append(synchronize_plugins_and_report_errors())
646
-
647
- loop.run_until_complete(asyncio.gather(*coroutines))
627
+ server.wait_for_termination()
648
628
  except KeyboardInterrupt:
649
629
  pass
650
630
  finally:
651
- loop.run_until_complete(*_cleanup_coroutines)
652
- loop.close()
631
+ executor.shutdown(wait=True, cancel_futures=True)
632
+ if synchronizer_thread.is_alive():
633
+ STOP_SYNCHRONIZER.set()
634
+ synchronizer_thread.join()
653
635
 
654
636
 
655
637
  if __name__ == "__main__":
plugin_runner/sandbox.py CHANGED
@@ -81,6 +81,7 @@ SAFE_EXTERNAL_DUNDER_READ_ATTRIBUTES = {
81
81
  }
82
82
 
83
83
  CANVAS_TOP_LEVEL_MODULES = (
84
+ "canvas_sdk.caching",
84
85
  "canvas_sdk.commands",
85
86
  "canvas_sdk.effects",
86
87
  "canvas_sdk.events",
@@ -233,6 +233,8 @@ enum EffectType {
233
233
  COMMIT_ADJUST_PRESCRIPTION_COMMAND = 1303;
234
234
  ENTER_IN_ERROR_ADJUST_PRESCRIPTION_COMMAND = 1304;
235
235
 
236
+ ORIGINATE_CHART_SECTION_REVIEW_COMMAND = 1400;
237
+
236
238
  SHOW_ACTION_BUTTON = 1000;
237
239
 
238
240
  PATIENT_PORTAL__FORM_RESULT = 2000;
@@ -260,6 +262,9 @@ enum EffectType {
260
262
  LAUNCH_MODAL = 3000;
261
263
 
262
264
  SIMPLE_API_RESPONSE = 4000;
265
+ SIMPLE_API_WEBSOCKET_ACCEPT = 4001;
266
+ SIMPLE_API_WEBSOCKET_DENY = 4002;
267
+ SIMPLE_API_WEBSOCKET_BROADCAST = 4003;
263
268
 
264
269
  UPDATE_USER = 5000;
265
270
 
@@ -268,6 +273,7 @@ enum EffectType {
268
273
  CREATE_SCHEDULE_EVENT = 6002;
269
274
 
270
275
  CREATE_PATIENT = 6003;
276
+
271
277
  }
272
278
 
273
279
  message Effect {
@@ -244,18 +244,12 @@ enum EventType {
244
244
  CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__PRE_SEARCH = 8012;
245
245
  CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__POST_SEARCH = 8013;
246
246
 
247
- // CHART_SECTION_REVIEW_COMMAND__PRE_ORIGINATE = 9000;
248
- // CHART_SECTION_REVIEW_COMMAND__POST_ORIGINATE = 9001;
249
- // CHART_SECTION_REVIEW_COMMAND__PRE_UPDATE = 9002;
250
- // CHART_SECTION_REVIEW_COMMAND__POST_UPDATE = 9003;
251
- // CHART_SECTION_REVIEW_COMMAND__PRE_COMMIT = 9004;
252
- // CHART_SECTION_REVIEW_COMMAND__POST_COMMIT = 9005;
253
- // CHART_SECTION_REVIEW_COMMAND__PRE_DELETE = 9006;
254
- // CHART_SECTION_REVIEW_COMMAND__POST_DELETE = 9007;
255
- // CHART_SECTION_REVIEW_COMMAND__PRE_ENTER_IN_ERROR = 9008;
256
- // CHART_SECTION_REVIEW_COMMAND__POST_ENTER_IN_ERROR = 9009;
257
- // CHART_SECTION_REVIEW_COMMAND__PRE_EXECUTE_ACTION = 9010;
258
- // CHART_SECTION_REVIEW_COMMAND__POST_EXECUTE_ACTION = 9011;
247
+ CHART_SECTION_REVIEW_COMMAND__PRE_ORIGINATE = 9000;
248
+ CHART_SECTION_REVIEW_COMMAND__POST_ORIGINATE = 9001;
249
+ CHART_SECTION_REVIEW_COMMAND__PRE_ENTER_IN_ERROR = 9002;
250
+ CHART_SECTION_REVIEW_COMMAND__POST_ENTER_IN_ERROR = 9003;
251
+ CHART_SECTION_REVIEW_COMMAND__PRE_EXECUTE_ACTION = 9004;
252
+ CHART_SECTION_REVIEW_COMMAND__POST_EXECUTE_ACTION = 9005;
259
253
 
260
254
 
261
255
  CLIPBOARD_COMMAND__PRE_ORIGINATE = 53000;
@@ -1093,6 +1087,8 @@ enum EventType {
1093
1087
 
1094
1088
  SIMPLE_API_AUTHENTICATE = 130000;
1095
1089
  SIMPLE_API_REQUEST = 130001;
1090
+ SIMPLE_API_WEBSOCKET_AUTHENTICATE = 130002;
1091
+
1096
1092
  }
1097
1093
 
1098
1094
  message Event {
pubsub/pubsub.py CHANGED
@@ -2,6 +2,9 @@ import os
2
2
  from typing import Any
3
3
 
4
4
  import redis
5
+ from redis.backoff import ExponentialBackoff
6
+ from redis.exceptions import ConnectionError, TimeoutError
7
+ from redis.retry import Retry
5
8
 
6
9
  REDIS_ENDPOINT = os.getenv("REDIS_ENDPOINT")
7
10
  CUSTOMER_IDENTIFIER = os.getenv("CUSTOMER_IDENTIFIER")
@@ -24,7 +27,13 @@ class PubSubBase:
24
27
 
25
28
  def _create_client(self) -> redis.Redis | None:
26
29
  if self.redis_endpoint and self.channel:
27
- return redis.Redis.from_url(self.redis_endpoint, decode_responses=True)
30
+ return redis.Redis.from_url(
31
+ self.redis_endpoint,
32
+ decode_responses=True,
33
+ retry=Retry(backoff=ExponentialBackoff(), retries=10),
34
+ retry_on_error=[ConnectionError, TimeoutError, ConnectionResetError],
35
+ health_check_interval=1,
36
+ )
28
37
 
29
38
  return None
30
39
 
settings.py CHANGED
@@ -9,6 +9,7 @@ from env_tools import env_to_bool
9
9
  load_dotenv()
10
10
 
11
11
  BASE_DIR = Path(__file__).resolve().parent.resolve()
12
+ FOURTEEN_DAYS = 60 * 60 * 24 * 14
12
13
 
13
14
  ENV = os.getenv("ENV", "development")
14
15
  IS_PRODUCTION = ENV == "production"
@@ -24,6 +25,10 @@ INTEGRATION_TEST_CLIENT_SECRET = os.getenv("INTEGRATION_TEST_CLIENT_SECRET")
24
25
 
25
26
  GRAPHQL_ENDPOINT = os.getenv("GRAPHQL_ENDPOINT", "http://localhost:8000/plugins-graphql")
26
27
  REDIS_ENDPOINT = os.getenv("REDIS_ENDPOINT", f"redis://{APP_NAME}-redis:6379")
28
+ CANVAS_SDK_PLUGINS_CACHE_LOCATION = os.getenv(
29
+ "CANVAS_SDK_PLUGINS_CACHE_LOCATION", "plugin_io_plugins_cache"
30
+ )
31
+ CANVAS_SDK_CACHE_TIMEOUT_SECONDS = int(os.getenv("CANVAS_SDK_CACHE_TIMEOUT", FOURTEEN_DAYS))
27
32
 
28
33
 
29
34
  METRICS_ENABLED = env_to_bool("PLUGINS_METRICS_ENABLED", not IS_SCRIPT)
@@ -46,12 +51,15 @@ CANVAS_SDK_DB_PASSWORD = os.getenv("CANVAS_SDK_DB_PASSWORD", "app")
46
51
  CANVAS_SDK_DB_HOST = os.getenv("CANVAS_SDK_DB_HOST", "home-app-db")
47
52
  CANVAS_SDK_DB_PORT = os.getenv("CANVAS_SDK_DB_PORT", "5432")
48
53
 
54
+ PLUGIN_RUNNER_MAX_WORKERS = int(os.getenv("PLUGIN_RUNNER_MAX_WORKERS", 5))
55
+
49
56
  if os.getenv("DATABASE_URL"):
50
57
  parsed_url = parse.urlparse(os.getenv("DATABASE_URL"))
51
58
 
52
59
  DATABASES = {
53
60
  "default": {
54
61
  "ENGINE": "django.db.backends.postgresql",
62
+ "OPTIONS": {"pool": {"min_size": 2, "max_size": PLUGIN_RUNNER_MAX_WORKERS}},
55
63
  "NAME": parsed_url.path[1:],
56
64
  "USER": os.getenv("CANVAS_SDK_DATABASE_ROLE"),
57
65
  "PASSWORD": os.getenv("CANVAS_SDK_DATABASE_ROLE_PASSWORD"),
@@ -63,6 +71,7 @@ else:
63
71
  DATABASES = {
64
72
  "default": {
65
73
  "ENGINE": "django.db.backends.postgresql",
74
+ "OPTIONS": {"pool": {"min_size": 2, "max_size": PLUGIN_RUNNER_MAX_WORKERS}},
66
75
  "NAME": CANVAS_SDK_DB_NAME,
67
76
  "USER": CANVAS_SDK_DB_USERNAME,
68
77
  "PASSWORD": CANVAS_SDK_DB_PASSWORD,
@@ -106,3 +115,26 @@ TEMPLATES = [
106
115
  "OPTIONS": {},
107
116
  },
108
117
  ]
118
+
119
+
120
+ if IS_TESTING:
121
+ CACHES = {
122
+ "default": {
123
+ "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
124
+ },
125
+ "plugins": {
126
+ "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
127
+ "KEY_PREFIX": CUSTOMER_IDENTIFIER,
128
+ "LOCATION": "plugins_cache",
129
+ "TIMEOUT": CANVAS_SDK_CACHE_TIMEOUT_SECONDS,
130
+ },
131
+ }
132
+ else:
133
+ CACHES = {
134
+ "plugins": {
135
+ "BACKEND": "django.core.cache.backends.db.DatabaseCache",
136
+ "KEY_PREFIX": CUSTOMER_IDENTIFIER,
137
+ "LOCATION": CANVAS_SDK_PLUGINS_CACHE_LOCATION,
138
+ "TIMEOUT": CANVAS_SDK_CACHE_TIMEOUT_SECONDS,
139
+ }
140
+ }