|
| 1 | +# canvas-plugins |
| 2 | + |
| 3 | +event-driven plugin SDK and runtime for Canvas Medical EHR. published as the `canvas` PyPI package. |
| 4 | + |
| 5 | +## project layout |
| 6 | + |
| 7 | +- `canvas_sdk/` - SDK for plugin authors (handlers, effects, commands, events, data models) |
| 8 | +- `plugin_runner/` - gRPC service that loads/sandboxes/executes plugins via `RestrictedPython` |
| 9 | +- `canvas_cli/` - typer CLI (`canvas init`, `canvas install`, `canvas emit`, etc.) |
| 10 | +- `canvas_generated/` - auto-generated protobuf python; **do not edit directly** |
| 11 | +- `protobufs/` - source `.proto` definitions |
| 12 | +- `pubsub/` - redis pub/sub for plugin reload signals and log streaming |
| 13 | +- `logger/` - structured logging with logstash + redis |
| 14 | +- `example-plugins/` - tens of reference plugins |
| 15 | +- `settings.py` - django settings (sqlite in tests, postgres in prod) |
| 16 | +- `conftest.py` - root pytest fixtures |
| 17 | + |
| 18 | +## companion repo |
| 19 | + |
| 20 | +`../canvas/home-app/` is the main Canvas app. cross-repo changes are common: |
| 21 | +- protobuf changes here must be copied to home-app's `canvas_generated/` |
| 22 | +- new SDK data models (`canvas_sdk/v1/data/`) mirror home-app Django models (read-only) |
| 23 | +- home-app's `plugin_io/database_views.py` exposes DB views for SDK models |
| 24 | +- run `bin/generate-protobufs` after changing `.proto` files |
| 25 | + |
| 26 | +## key architecture |
| 27 | + |
| 28 | +- plugins declare handled events in `CANVAS_MANIFEST.json` |
| 29 | +- `plugin_runner.plugin_runner.EVENT_HANDLER_MAP` maps event names → handler classes |
| 30 | +- `LOADED_PLUGINS` tracks installed plugin metadata |
| 31 | +- handlers extend `BaseHandler`, implement `compute()` returning effects |
| 32 | +- prefer "handlers" over "protocols" everywhere: use `handlers` key in `CANVAS_MANIFEST.json`, `BaseHandler` over `BaseProtocol`, `canvas_sdk.handlers` over `canvas_sdk.protocols`. the `protocols` names still work but are legacy |
| 33 | +- plugin code runs in a `RestrictedPython` sandbox with an allowed-imports whitelist |
| 34 | +- `plugin_runner/allowed-module-imports.json` is auto-generated by pre-commit |
| 35 | + |
| 36 | +## tooling |
| 37 | + |
| 38 | +- **python**: 3.11, 3.12 (requires >=3.11, <3.13) |
| 39 | +- **package manager**: `uv` (>=0.8.0) |
| 40 | +- **build**: hatchling |
| 41 | +- **release**: python-semantic-release, tag-based versioning |
| 42 | + |
| 43 | +## commands |
| 44 | + |
| 45 | +```sh |
| 46 | +uv run pytest -m "not integtest" # unit tests |
| 47 | +uv run pytest path/to/test.py -k name # specific test |
| 48 | +uv run mypy canvas_sdk/ plugin_runner/ # type checking (or use bin/mypy) |
| 49 | +uv run ruff check --fix . # lint |
| 50 | +uv run ruff format . # format |
| 51 | +bin/generate-protobufs # regenerate protobuf python |
| 52 | +uv run python -m plugin_runner.generate_allowed_imports # update import whitelist |
| 53 | +``` |
| 54 | + |
| 55 | +## code style |
| 56 | + |
| 57 | +- prefer simple test functions over test classes |
| 58 | +- use `pydantic` for command validation, real types over `Any` |
| 59 | + |
| 60 | +## testing |
| 61 | + |
| 62 | +- pytest with django plugin, uses sqlite in test mode |
| 63 | +- `conftest.py` fixtures: `install_test_plugin`, `load_test_plugins`, `cli_runner` |
| 64 | +- plugin test fixtures live in `plugin_runner/tests/fixtures/plugins/` |
| 65 | +- CI tests against python 3.11 and 3.12 matrix |
| 66 | +- example plugins each tested independently in CI |
| 67 | +- integration tests dispatched to the canvas repo |
0 commit comments