canvas 0.1.4__py3-none-any.whl → 0.1.6__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 (70) hide show
  1. canvas-0.1.6.dist-info/METADATA +176 -0
  2. canvas-0.1.6.dist-info/RECORD +66 -0
  3. {canvas-0.1.4.dist-info → canvas-0.1.6.dist-info}/WHEEL +1 -1
  4. canvas-0.1.6.dist-info/entry_points.txt +3 -0
  5. canvas_cli/apps/__init__.py +0 -0
  6. canvas_cli/apps/auth/__init__.py +3 -0
  7. canvas_cli/apps/auth/tests.py +142 -0
  8. canvas_cli/apps/auth/utils.py +163 -0
  9. canvas_cli/apps/logs/__init__.py +3 -0
  10. canvas_cli/apps/logs/logs.py +59 -0
  11. canvas_cli/apps/plugin/__init__.py +9 -0
  12. canvas_cli/apps/plugin/plugin.py +286 -0
  13. canvas_cli/apps/plugin/tests.py +32 -0
  14. canvas_cli/conftest.py +28 -0
  15. canvas_cli/main.py +78 -0
  16. canvas_cli/templates/plugins/default/cookiecutter.json +4 -0
  17. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json +29 -0
  18. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/README.md +12 -0
  19. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/__init__.py +0 -0
  20. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +55 -0
  21. canvas_cli/tests.py +11 -0
  22. canvas_cli/utils/__init__.py +0 -0
  23. canvas_cli/utils/context/__init__.py +3 -0
  24. canvas_cli/utils/context/context.py +172 -0
  25. canvas_cli/utils/context/tests.py +130 -0
  26. canvas_cli/utils/print/__init__.py +3 -0
  27. canvas_cli/utils/print/print.py +60 -0
  28. canvas_cli/utils/print/tests.py +70 -0
  29. canvas_cli/utils/urls/__init__.py +3 -0
  30. canvas_cli/utils/urls/tests.py +12 -0
  31. canvas_cli/utils/urls/urls.py +27 -0
  32. canvas_cli/utils/validators/__init__.py +3 -0
  33. canvas_cli/utils/validators/manifest_schema.py +80 -0
  34. canvas_cli/utils/validators/tests.py +36 -0
  35. canvas_cli/utils/validators/validators.py +40 -0
  36. canvas_sdk/__init__.py +0 -0
  37. canvas_sdk/commands/__init__.py +27 -0
  38. canvas_sdk/commands/base.py +118 -0
  39. canvas_sdk/commands/commands/assess.py +48 -0
  40. canvas_sdk/commands/commands/diagnose.py +44 -0
  41. canvas_sdk/commands/commands/goal.py +48 -0
  42. canvas_sdk/commands/commands/history_present_illness.py +15 -0
  43. canvas_sdk/commands/commands/medication_statement.py +28 -0
  44. canvas_sdk/commands/commands/plan.py +15 -0
  45. canvas_sdk/commands/commands/prescribe.py +48 -0
  46. canvas_sdk/commands/commands/questionnaire.py +17 -0
  47. canvas_sdk/commands/commands/reason_for_visit.py +36 -0
  48. canvas_sdk/commands/commands/stop_medication.py +18 -0
  49. canvas_sdk/commands/commands/update_goal.py +48 -0
  50. canvas_sdk/commands/constants.py +9 -0
  51. canvas_sdk/commands/tests/test_utils.py +195 -0
  52. canvas_sdk/commands/tests/tests.py +407 -0
  53. canvas_sdk/data/__init__.py +0 -0
  54. canvas_sdk/effects/__init__.py +1 -0
  55. canvas_sdk/effects/banner_alert/banner_alert.py +37 -0
  56. canvas_sdk/effects/banner_alert/constants.py +19 -0
  57. canvas_sdk/effects/base.py +30 -0
  58. canvas_sdk/events/__init__.py +1 -0
  59. canvas_sdk/protocols/__init__.py +1 -0
  60. canvas_sdk/protocols/base.py +12 -0
  61. canvas_sdk/tests/__init__.py +0 -0
  62. canvas_sdk/utils/__init__.py +3 -0
  63. canvas_sdk/utils/http.py +72 -0
  64. canvas_sdk/utils/tests.py +63 -0
  65. canvas_sdk/views/__init__.py +0 -0
  66. canvas/main.py +0 -19
  67. canvas-0.1.4.dist-info/METADATA +0 -285
  68. canvas-0.1.4.dist-info/RECORD +0 -6
  69. canvas-0.1.4.dist-info/entry_points.txt +0 -3
  70. {canvas → canvas_cli}/__init__.py +0 -0
@@ -0,0 +1,286 @@
1
+ import json
2
+ import tarfile
3
+ import tempfile
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from urllib.parse import urljoin
7
+
8
+ import requests
9
+ import typer
10
+ from cookiecutter.exceptions import OutputDirExistsException
11
+ from cookiecutter.main import cookiecutter
12
+
13
+ from canvas_cli.apps.auth.utils import get_default_host, get_or_request_api_token
14
+ from canvas_cli.utils.context import context
15
+ from canvas_cli.utils.print import print
16
+ from canvas_cli.utils.validators import validate_manifest_file
17
+
18
+
19
+ def plugin_url(host: str, *paths: str) -> str:
20
+ """Generates the plugin url for managing plugins in a Canvas instance."""
21
+ join = "/".join(["plugin-io/plugins", "/".join(paths or [])])
22
+ join = join if join.endswith("/") else join + "/"
23
+
24
+ return urljoin(host, join)
25
+
26
+
27
+ def validate_package(package: Path) -> Path:
28
+ """Validate if `package` Path exists and it is a file."""
29
+ if not package.exists():
30
+ raise typer.BadParameter(f"Package {package} does not exist")
31
+
32
+ if not package.is_file():
33
+ raise typer.BadParameter(f"Package {package} isn't a file")
34
+
35
+ if not package.name.endswith("tar.gz") and not package.name.endswith("whl"):
36
+ raise typer.BadParameter(f"Package {package} needs to be a tar.gz or a whl")
37
+
38
+ return package
39
+
40
+
41
+ def _build_package(package: Path) -> Path:
42
+ """Runs `poetry build` on `package` and returns the built archive."""
43
+ if not package.exists() or not package.is_dir():
44
+ raise typer.BadParameter(f"Couldn't build {package}, not a dir")
45
+
46
+ with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as file:
47
+ with tarfile.open(file.name, "w:gz") as tar:
48
+ tar.add(package, arcname=".")
49
+
50
+ return Path(file.name)
51
+
52
+
53
+ def _get_name_from_metadata(host: str, token: str, package: Path) -> Optional[str]:
54
+ """Extract metadata from a provided package and return the package name if it exists in the metadata."""
55
+ try:
56
+ metadata_response = requests.post(
57
+ plugin_url(host, "extract-metadata"),
58
+ headers={"Authorization": f"Bearer {token}"},
59
+ files={"package": open(package, "rb")},
60
+ )
61
+ except requests.exceptions.RequestException:
62
+ print.json(f"Failed to connect to {host}", success=False)
63
+ raise typer.Exit(1)
64
+
65
+ if metadata_response.status_code != requests.codes.ok:
66
+ print.response(metadata_response, success=False)
67
+ raise typer.Exit(1)
68
+
69
+ metadata = metadata_response.json()
70
+ return metadata.get("name")
71
+
72
+
73
+ def get_base_plugin_template_path() -> Path:
74
+ """Return context's base_plugin_template_path, so it can be used as a Typer default."""
75
+ return context.plugin_template_dir / context.default_plugin_template_name
76
+
77
+
78
+ def init() -> None:
79
+ """Create a new plugin."""
80
+ template = get_base_plugin_template_path()
81
+ try:
82
+ project_dir = cookiecutter(str(template))
83
+ except OutputDirExistsException:
84
+ raise typer.BadParameter(f"The supplied directory already exists")
85
+
86
+ print.json(f"Project created in {project_dir}", project_dir=project_dir)
87
+
88
+
89
+ def install(
90
+ plugin_name: Path = typer.Argument(..., help="Path to plugin to install"),
91
+ host: Optional[str] = typer.Option(
92
+ callback=get_default_host,
93
+ help="Canvas instance to connect to",
94
+ default=None,
95
+ ),
96
+ ) -> None:
97
+ """Install a plugin into a Canvas instance."""
98
+ if not host:
99
+ raise typer.BadParameter("Please specify a host or add one to the configuration file")
100
+
101
+ token = get_or_request_api_token(host)
102
+
103
+ if not plugin_name.exists():
104
+ raise typer.BadParameter(f"Plugin '{plugin_name}' does not exist")
105
+
106
+ if plugin_name.is_dir():
107
+ validate_manifest(plugin_name)
108
+ built_package_path = _build_package(plugin_name)
109
+ else:
110
+ raise typer.BadParameter(f"Plugin '{plugin_name}' needs to be a valid directory")
111
+
112
+ print.verbose(f"Installing plugin: {built_package_path} into {host}")
113
+
114
+ url = plugin_url(host)
115
+
116
+ print.verbose(f"Posting {built_package_path.absolute()} to {url}")
117
+
118
+ try:
119
+ r = requests.post(
120
+ url,
121
+ data={"is_enabled": True},
122
+ files={"package": open(built_package_path, "rb")},
123
+ headers={"Authorization": f"Bearer {token}"},
124
+ )
125
+ except requests.exceptions.RequestException:
126
+ print.json(f"Failed to connect to {host}", success=False)
127
+ raise typer.Exit(1)
128
+
129
+ if r.status_code == requests.codes.created:
130
+ print.response(r)
131
+
132
+ # If we got a bad_request, means there's a duplicate plugin and install can't handle that.
133
+ # So we need to get the plugin-name from the package and call `update` directly
134
+ elif r.status_code == requests.codes.bad_request:
135
+ plugin_name = _get_name_from_metadata(host, token, built_package_path)
136
+ update(plugin_name, built_package_path, is_enabled=True, host=host)
137
+ else:
138
+ print.response(r, success=False)
139
+ raise typer.Exit(1)
140
+
141
+
142
+ def uninstall(
143
+ name: str = typer.Argument(..., help="Plugin name to uninstall"),
144
+ host: Optional[str] = typer.Option(
145
+ callback=get_default_host,
146
+ help="Canvas instance to connect to",
147
+ default=None,
148
+ ),
149
+ ) -> None:
150
+ """Uninstall a plugin from a Canvas instance."""
151
+ if not host:
152
+ raise typer.BadParameter("Please specify a host or or add one to the configuration file")
153
+
154
+ url = plugin_url(host, name)
155
+
156
+ print.verbose(f"Uninstalling {name} using {url}")
157
+
158
+ token = get_or_request_api_token(host)
159
+
160
+ try:
161
+ r = requests.delete(
162
+ url,
163
+ headers={
164
+ "Authorization": f"Bearer {token}",
165
+ },
166
+ )
167
+ except requests.exceptions.RequestException:
168
+ print.json(f"Failed to connect to {host}", success=False)
169
+ raise typer.Exit(1)
170
+
171
+ if r.status_code == requests.codes.no_content:
172
+ print.response(r)
173
+ else:
174
+ print.response(r, success=False)
175
+ raise typer.Exit(1)
176
+
177
+
178
+ def list(
179
+ host: Optional[str] = typer.Option(
180
+ callback=get_default_host,
181
+ help="Canvas instance to connect to",
182
+ default=None,
183
+ )
184
+ ) -> None:
185
+ """List all plugins from a Canvas instance."""
186
+ if not host:
187
+ raise typer.BadParameter("Please specify a host or add one to the configuration file")
188
+
189
+ url = plugin_url(host)
190
+
191
+ token = get_or_request_api_token(host)
192
+
193
+ try:
194
+ r = requests.get(
195
+ url,
196
+ headers={"Authorization": f"Bearer {token}"},
197
+ )
198
+ except requests.exceptions.RequestException:
199
+ print.json(f"Failed to connect to {host}", success=False)
200
+ raise typer.Exit(1)
201
+
202
+ if r.status_code == requests.codes.ok:
203
+ print.response(r)
204
+ else:
205
+ print.response(r, success=False)
206
+ raise typer.Exit(1)
207
+
208
+
209
+ def validate_manifest(
210
+ plugin_name: Path = typer.Argument(..., help="Path to plugin to validate"),
211
+ ) -> None:
212
+ """Validate the Canvas Manifest json file."""
213
+ if not plugin_name.exists():
214
+ raise typer.BadParameter(f"Plugin '{plugin_name}' does not exist")
215
+
216
+ if not plugin_name.is_dir():
217
+ raise typer.BadParameter(f"Plugin '{plugin_name}' is not a directory, nothing to validate")
218
+
219
+ manifest = plugin_name / "CANVAS_MANIFEST.json"
220
+
221
+ if not manifest.exists():
222
+ raise typer.BadParameter(
223
+ f"Plugin '{plugin_name}' does not have a CANVAS_MANIFEST.json file to validate"
224
+ )
225
+
226
+ try:
227
+ manifest_json = json.loads(manifest.read_text())
228
+ except json.JSONDecodeError:
229
+ print.json(
230
+ "There was a problem loading the manifest file, please ensure it's valid JSON",
231
+ success=False,
232
+ path=str(plugin_name),
233
+ )
234
+ raise typer.Abort()
235
+
236
+ validate_manifest_file(manifest_json)
237
+
238
+ print.json(f"Plugin '{plugin_name}' has a valid CANVAS_MANIFEST.json file")
239
+
240
+
241
+ def update(
242
+ name: str = typer.Argument(..., help="Plugin name to update"),
243
+ package: Optional[Path] = typer.Option(
244
+ help="Path to a wheel or sdist file containing the python package to install",
245
+ default=None,
246
+ ),
247
+ is_enabled: Optional[bool] = typer.Option(
248
+ None, "--enable/--disable", show_default=False, help="Enable/disable the plugin"
249
+ ),
250
+ host: Optional[str] = typer.Option(
251
+ callback=get_default_host,
252
+ help="Canvas instance to connect to",
253
+ default=None,
254
+ ),
255
+ ) -> None:
256
+ """Updates a plugin from an instance."""
257
+ if not host:
258
+ raise typer.BadParameter("Please specify a host or set a default via the `auth` command")
259
+
260
+ if package:
261
+ validate_package(package)
262
+
263
+ token = get_or_request_api_token(host)
264
+
265
+ print.verbose(f"Updating plugin {name} from {host} with {is_enabled=}, {package=}")
266
+
267
+ binary_package = {"package": open(package, "rb")} if package else None
268
+
269
+ url = plugin_url(host, name)
270
+
271
+ try:
272
+ r = requests.patch(
273
+ url,
274
+ data={"is_enabled": is_enabled} if is_enabled is not None else {},
275
+ files=binary_package,
276
+ headers={"Authorization": f"Bearer {token}"},
277
+ )
278
+ except requests.exceptions.RequestException:
279
+ print.json(f"Failed to connect to {host}", success=False)
280
+ raise typer.Exit(1)
281
+
282
+ if r.status_code == requests.codes.ok:
283
+ print.response(r)
284
+ else:
285
+ print.response(r, success=False)
286
+ raise typer.Exit(1)
@@ -0,0 +1,32 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+ import typer
5
+ from typer.testing import CliRunner
6
+
7
+ from .plugin import validate_package
8
+
9
+ runner = CliRunner()
10
+
11
+
12
+ def test_validate_package_unexistant_path() -> None:
13
+ """Tests the validate_package callback with an invalid folder."""
14
+ with pytest.raises(typer.BadParameter):
15
+ validate_package(Path("/a_random_url_that_will_not_exist_or_so_I_hope"))
16
+
17
+
18
+ def test_validate_package_wrong_file_type(tmp_path: Path) -> None:
19
+ """Tests the validate_package callback with an invalid file type."""
20
+ invalid_file = tmp_path / "tmp_file.zip"
21
+ invalid_file.write_text("definitely not a python package")
22
+
23
+ with pytest.raises(typer.BadParameter):
24
+ validate_package(invalid_file)
25
+
26
+
27
+ def test_validate_package_valid_file(tmp_path: Path) -> None:
28
+ """Tests the validate_package callback with a valid file type."""
29
+ package_path = tmp_path / "test-package.whl"
30
+ package_path.write_text("something")
31
+ result = validate_package(package_path)
32
+ assert result == package_path
canvas_cli/conftest.py ADDED
@@ -0,0 +1,28 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ import canvas_cli.main
6
+ from canvas_cli.utils.context import context
7
+
8
+
9
+ @pytest.fixture(autouse=True)
10
+ def monkeypatch_app_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
11
+ """Monkeypatch `get_app_dir` in order to return a temp dir when testing, so we don't overwrite our config file."""
12
+
13
+ def app_dir() -> str:
14
+ return str(tmp_path)
15
+
16
+ monkeypatch.setattr(canvas_cli.main, "get_app_dir", app_dir)
17
+
18
+
19
+ @pytest.fixture(autouse=True)
20
+ def reset_context_variables() -> None:
21
+ """Reset the context properties to their default value.
22
+ This is needed because we cannot build a `reset` method in the CLIContext class,
23
+ because `load_from_file` loads properties dynamically.
24
+ Also since this is a CLI, it's not expected to keep the global context in memory for more than a run,
25
+ which definitely happens with tests run
26
+ """
27
+ context._default_host = None
28
+ context._verbose = False
canvas_cli/main.py ADDED
@@ -0,0 +1,78 @@
1
+ import importlib.metadata
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ import typer
6
+
7
+ from canvas_cli.apps import plugin
8
+ from canvas_cli.apps.logs import logs as logs_command
9
+ from canvas_cli.utils.context import context
10
+ from canvas_cli.utils.print import print
11
+
12
+ APP_NAME = "canvas_cli"
13
+
14
+ # The main app
15
+ app = typer.Typer(no_args_is_help=True)
16
+
17
+ # Commands
18
+ app.command(short_help="Create a new plugin")(plugin.init)
19
+ app.command(short_help="Install a plugin into a Canvas instance")(plugin.install)
20
+ app.command(short_help="Uninstall a plugin from a Canvas instance")(plugin.uninstall)
21
+ app.command(short_help="List all plugins from a Canvas instance")(plugin.list)
22
+ app.command(short_help="Validate the Canvas Manifest json file")(plugin.validate_manifest)
23
+ app.command(short_help="Listen and print log streams from a Canvas instance")(logs_command)
24
+
25
+ # Our current version
26
+ __version__ = importlib.metadata.version("canvas")
27
+
28
+
29
+ def version_callback(value: bool) -> None:
30
+ """Method called when the `--version` flag is set. Prints the version and exits the CLI."""
31
+ if value:
32
+ print.json(f"{APP_NAME} Version: {__version__}", version=__version__)
33
+ raise typer.Exit()
34
+
35
+
36
+ def get_app_dir() -> str:
37
+ """Return the app dir, where the config file will be saved.
38
+ This method is monkeypatched in conftest.py, for testing purposes.
39
+ """
40
+ return typer.get_app_dir(APP_NAME)
41
+
42
+
43
+ def get_or_create_config_file() -> Path:
44
+ """Method called to get a Path to the existent JSON config file, or create one if it doesn't exist."""
45
+ app_dir = get_app_dir()
46
+ config_path: Path = Path(app_dir) / "config.json"
47
+ if not config_path.is_file():
48
+ Path(app_dir).mkdir(parents=True, exist_ok=True)
49
+ with open(config_path, "w+") as file:
50
+ file.write("{}")
51
+
52
+ return config_path
53
+
54
+
55
+ @app.callback()
56
+ def main(
57
+ no_ansi: bool = typer.Option(False, "--no-ansi", help="Disable colorized output"),
58
+ version: Optional[bool] = typer.Option(
59
+ None, "--version", callback=version_callback, is_eager=True
60
+ ),
61
+ verbose: bool = typer.Option(False, "--verbose", help="Show extra output"),
62
+ ) -> None:
63
+ """Canvas swiss army knife CLI tool."""
64
+ # Fetch the config file and load our context from it.
65
+ config_file = get_or_create_config_file()
66
+
67
+ context.load_from_file(config_file)
68
+
69
+ context.no_ansi = no_ansi
70
+
71
+ # Set the --verbose flag
72
+ if verbose:
73
+ context.verbose = verbose
74
+ print.verbose("Verbose mode enabled")
75
+
76
+
77
+ if __name__ == "__main__":
78
+ app()
@@ -0,0 +1,4 @@
1
+ {
2
+ "project_name": "My Cool Plugin",
3
+ "__project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_') }}"
4
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "sdk_version": "0.1.4",
3
+ "plugin_version": "0.0.1",
4
+ "name": "{{ cookiecutter.__project_slug }}",
5
+ "description": "Edit the description in CANVAS_MANIFEST.json",
6
+ "components": {
7
+ "protocols": [
8
+ {
9
+ "class": "{{ cookiecutter.__project_slug }}.protocols.my_protocol:Protocol",
10
+ "description": "A protocol that does xyz...",
11
+ "data_access": {
12
+ "event": "",
13
+ "read": [],
14
+ "write": []
15
+ }
16
+ }
17
+ ],
18
+ "commands": [],
19
+ "content": [],
20
+ "effects": [],
21
+ "views": []
22
+ },
23
+ "secrets": [],
24
+ "tags": {},
25
+ "references": [],
26
+ "license": "",
27
+ "diagram": false,
28
+ "readme": "./README.md"
29
+ }
@@ -0,0 +1,12 @@
1
+ {% for _ in cookiecutter.__project_slug %}={% endfor %}
2
+ {{ cookiecutter.__project_slug }}
3
+ {% for _ in cookiecutter.__project_slug %}={% endfor %}
4
+
5
+ ## Description
6
+
7
+ A description of this plugin
8
+
9
+ ### Important Note!
10
+
11
+ The CANVAS_MANIFEST.json is used when installing your plugin. Please ensure it
12
+ gets updated if you add, remove, or rename protocols.
@@ -0,0 +1,55 @@
1
+ from canvas_sdk.events import EventType
2
+ from canvas_sdk.protocols import BaseProtocol
3
+ from logger import log
4
+
5
+
6
+ # Inherit from BaseProtocol to properly get registered for events
7
+ class Protocol(BaseProtocol):
8
+ """
9
+ You should put a helpful description of this protocol's behavior here.
10
+ """
11
+
12
+ # Name the event type you wish to run in response to
13
+ RESPONDS_TO = EventType.Name(EventType.ASSESS_COMMAND__CONDITION_SELECTED)
14
+
15
+ NARRATIVE_STRING = "I was inserted from my plugin's protocol."
16
+
17
+ def compute(self):
18
+ """
19
+ This method gets called when an event of the type RESPONDS_TO is fired.
20
+ """
21
+ # This class is initialized with several pieces of information you can
22
+ # access.
23
+ #
24
+ # `self.event` is the event object that caused this method to be
25
+ # called.
26
+ #
27
+ # `self.target` is an identifier for the object that is the subject of
28
+ # the event. In this case, it would be the identifier of the assess
29
+ # command. If this was a patient create event, it would be the
30
+ # identifier of the patient. If this was a task update event, it would
31
+ # be the identifier of the task. Etc, etc.
32
+ #
33
+ # `self.context` is a python dictionary of additional data that was
34
+ # given with the event. The information given here depends on the
35
+ # event type.
36
+ #
37
+ # `self.secrets` is a python dictionary of the secrets you defined in
38
+ # your CANVAS_MANIFEST.json and set values for in the uploaded
39
+ # plugin's configuration page: <emr_base_url>/admin/plugin_io/plugin/<plugin_id>/change/
40
+ # Example: self.secrets['WEBHOOK_URL']
41
+
42
+ # You can log things and see them using the Canvas CLI's log streaming
43
+ # function.
44
+ log.info(self.NARRATIVE_STRING)
45
+
46
+ # Craft a payload to be returned with the effect(s).
47
+ payload = {
48
+ "note": {"uuid": self.context["note"]["uuid"]},
49
+ "data": {"narrative": self.NARRATIVE_STRING},
50
+ }
51
+
52
+ # Return zero, one, or many effects.
53
+ # Example:
54
+ # return [Effect(type=EffectType.ADD_PLAN_COMMAND, payload=json.dumps(payload))]
55
+ return []
canvas_cli/tests.py ADDED
@@ -0,0 +1,11 @@
1
+ from typer.testing import CliRunner
2
+
3
+ from .main import app
4
+
5
+ runner = CliRunner()
6
+
7
+
8
+ def test_version_callback_exits_successfully() -> None:
9
+ """Tests the CLI exits with 0 when calling with `--version`."""
10
+ result = runner.invoke(app, "--version")
11
+ assert result.exit_code == 0
File without changes
@@ -0,0 +1,3 @@
1
+ from canvas_cli.utils.context.context import CLIContext, context
2
+
3
+ __all__ = ("context", "CLIContext")