diff --git a/README.md b/README.md index 155493c5..46fd144a 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,7 @@ $ canvas [OPTIONS] COMMAND [ARGS]... **Options**: -- `--no-ansi`: Disable colorized output - `--version` -- `--verbose`: Show extra output -- `--install-completion`: Install completion for the current shell. -- `--show-completion`: Show completion for the current shell, to copy it or customize the installation. - `--help`: Show this message and exit. **Commands**: diff --git a/canvas_cli/apps/logs/logs.py b/canvas_cli/apps/logs/logs.py index de0d2309..d768597c 100644 --- a/canvas_cli/apps/logs/logs.py +++ b/canvas_cli/apps/logs/logs.py @@ -1,3 +1,4 @@ +import json from typing import Optional from urllib.parse import urlparse @@ -5,23 +6,28 @@ import websocket from canvas_cli.apps.auth.utils import get_default_host, get_or_request_api_token -from canvas_cli.utils.print import print def _on_message(ws: websocket.WebSocketApp, message: str) -> None: - print.json(message) + message_to_print = message + try: + message_json = json.loads(message) + message_to_print = f"{message_json['timestamp']} | {message_json['message']}" + except ValueError: + pass + print(message_to_print) def _on_error(ws: websocket.WebSocketApp, error: str) -> None: - print.json(f"Error: {error}", success=False) + print(f"Error: {error}") def _on_close(ws: websocket.WebSocketApp, close_status_code: str, close_msg: str) -> None: - print.json(f"Connection closed with status code {close_status_code}: {close_msg}") + print(f"Connection closed with status code {close_status_code}: {close_msg}") def _on_open(ws: websocket.WebSocketApp) -> None: - print.json("Connected to the logging service") + print("Connected to the logging service") def logs( @@ -40,8 +46,8 @@ def logs( hostname = urlparse(host).hostname instance = hostname.removesuffix(".canvasmedical.com") - print.json( - f"Connecting to the log stream. Please be patient as there may be a delay before log messages appear." + print( + "Connecting to the log stream. Please be patient as there may be a delay before log messages appear." ) websocket_uri = f"wss://logs.console.canvasmedical.com/{instance}?token={token}" diff --git a/canvas_cli/apps/plugin/plugin.py b/canvas_cli/apps/plugin/plugin.py index 83fef346..fe22df73 100644 --- a/canvas_cli/apps/plugin/plugin.py +++ b/canvas_cli/apps/plugin/plugin.py @@ -12,7 +12,6 @@ from canvas_cli.apps.auth.utils import get_default_host, get_or_request_api_token from canvas_cli.utils.context import context -from canvas_cli.utils.print import print from canvas_cli.utils.validators import validate_manifest_file @@ -59,11 +58,11 @@ def _get_name_from_metadata(host: str, token: str, package: Path) -> Optional[st files={"package": open(package, "rb")}, ) except requests.exceptions.RequestException: - print.json(f"Failed to connect to {host}", success=False) + print(f"Failed to connect to {host}") raise typer.Exit(1) if metadata_response.status_code != requests.codes.ok: - print.response(metadata_response, success=False) + print(f"Status code {metadata_response.status_code}: {metadata_response.text}") raise typer.Exit(1) metadata = metadata_response.json() @@ -83,7 +82,7 @@ def init() -> None: except OutputDirExistsException: raise typer.BadParameter(f"The supplied directory already exists") - print.json(f"Project created in {project_dir}", project_dir=project_dir) + print(f"Project created in {project_dir}") def install( @@ -109,11 +108,11 @@ def install( else: raise typer.BadParameter(f"Plugin '{plugin_name}' needs to be a valid directory") - print.verbose(f"Installing plugin: {built_package_path} into {host}") + print(f"Installing plugin: {built_package_path} into {host}") url = plugin_url(host) - print.verbose(f"Posting {built_package_path.absolute()} to {url}") + print(f"Posting {built_package_path.absolute()} to {url}") try: r = requests.post( @@ -123,11 +122,11 @@ def install( headers={"Authorization": f"Bearer {token}"}, ) except requests.exceptions.RequestException: - print.json(f"Failed to connect to {host}", success=False) + print(f"Failed to connect to {host}") raise typer.Exit(1) if r.status_code == requests.codes.created: - print.response(r) + print("Plugin successfully installed!") # If we got a bad_request, means there's a duplicate plugin and install can't handle that. # So we need to get the plugin-name from the package and call `update` directly @@ -135,7 +134,7 @@ def install( plugin_name = _get_name_from_metadata(host, token, built_package_path) update(plugin_name, built_package_path, is_enabled=True, host=host) else: - print.response(r, success=False) + print(f"Status code {r.status_code}: {r.text}") raise typer.Exit(1) @@ -153,7 +152,7 @@ def uninstall( url = plugin_url(host, name) - print.verbose(f"Uninstalling {name} using {url}") + print(f"Uninstalling {name} using {url}") token = get_or_request_api_token(host) @@ -165,13 +164,13 @@ def uninstall( }, ) except requests.exceptions.RequestException: - print.json(f"Failed to connect to {host}", success=False) + print(f"Failed to connect to {host}") raise typer.Exit(1) if r.status_code == requests.codes.no_content: - print.response(r) + print(r.text) else: - print.response(r, success=False) + print(f"Status code {r.status_code}: {r.text}") raise typer.Exit(1) @@ -196,13 +195,16 @@ def list( headers={"Authorization": f"Bearer {token}"}, ) except requests.exceptions.RequestException: - print.json(f"Failed to connect to {host}", success=False) + print(f"Failed to connect to {host}") raise typer.Exit(1) if r.status_code == requests.codes.ok: - print.response(r) + for plugin in r.json().get("results", []): + print( + f"{plugin['name']}@{plugin['version']}\t{'enabled' if plugin['is_enabled'] else 'not enabled'}" + ) else: - print.response(r, success=False) + print(f"Status code {r.status_code}: {r.text}") raise typer.Exit(1) @@ -226,16 +228,12 @@ def validate_manifest( try: manifest_json = json.loads(manifest.read_text()) except json.JSONDecodeError: - print.json( - "There was a problem loading the manifest file, please ensure it's valid JSON", - success=False, - path=str(plugin_name), - ) + print("There was a problem loading the manifest file, please ensure it's valid JSON") raise typer.Abort() validate_manifest_file(manifest_json) - print.json(f"Plugin '{plugin_name}' has a valid CANVAS_MANIFEST.json file") + print(f"Plugin '{plugin_name}' has a valid CANVAS_MANIFEST.json file") def update( @@ -262,7 +260,7 @@ def update( token = get_or_request_api_token(host) - print.verbose(f"Updating plugin {name} from {host} with {is_enabled=}, {package=}") + print(f"Updating plugin {name} from {host} with {is_enabled=}, {package=}") binary_package = {"package": open(package, "rb")} if package else None @@ -276,11 +274,12 @@ def update( headers={"Authorization": f"Bearer {token}"}, ) except requests.exceptions.RequestException: - print.json(f"Failed to connect to {host}", success=False) + print(f"Failed to connect to {host}") raise typer.Exit(1) if r.status_code == requests.codes.ok: - print.response(r) + print("Plugin successfully updated!") + else: - print.response(r, success=False) + print(f"Status code {r.status_code}: {r.text}") raise typer.Exit(1) diff --git a/canvas_cli/conftest.py b/canvas_cli/conftest.py index 331ecbd0..037c6ded 100644 --- a/canvas_cli/conftest.py +++ b/canvas_cli/conftest.py @@ -25,4 +25,3 @@ def reset_context_variables() -> None: which definitely happens with tests run """ context._default_host = None - context._verbose = False diff --git a/canvas_cli/main.py b/canvas_cli/main.py index 2db302b4..e55aded1 100644 --- a/canvas_cli/main.py +++ b/canvas_cli/main.py @@ -7,12 +7,11 @@ from canvas_cli.apps import plugin from canvas_cli.apps.logs import logs as logs_command from canvas_cli.utils.context import context -from canvas_cli.utils.print import print APP_NAME = "canvas_cli" # The main app -app = typer.Typer(no_args_is_help=True) +app = typer.Typer(no_args_is_help=True, rich_markup_mode=None, add_completion=False) # Commands app.command(short_help="Create a new plugin")(plugin.init) @@ -29,7 +28,7 @@ def version_callback(value: bool) -> None: """Method called when the `--version` flag is set. Prints the version and exits the CLI.""" if value: - print.json(f"{APP_NAME} Version: {__version__}", version=__version__) + print(f"{APP_NAME} Version: {__version__}") raise typer.Exit() @@ -54,11 +53,9 @@ def get_or_create_config_file() -> Path: @app.callback() def main( - no_ansi: bool = typer.Option(False, "--no-ansi", help="Disable colorized output"), version: Optional[bool] = typer.Option( None, "--version", callback=version_callback, is_eager=True ), - verbose: bool = typer.Option(False, "--verbose", help="Show extra output"), ) -> None: """Canvas swiss army knife CLI tool.""" # Fetch the config file and load our context from it. @@ -66,13 +63,6 @@ def main( context.load_from_file(config_file) - context.no_ansi = no_ansi - - # Set the --verbose flag - if verbose: - context.verbose = verbose - print.verbose("Verbose mode enabled") - if __name__ == "__main__": app() diff --git a/canvas_cli/utils/context/context.py b/canvas_cli/utils/context/context.py index 56c0414c..8c60a836 100644 --- a/canvas_cli/utils/context/context.py +++ b/canvas_cli/utils/context/context.py @@ -41,12 +41,6 @@ class CLIContext: # The default host to use for requests _default_host: str | None = None - # Print extra output - _verbose: bool = False - - # If True no colored output is shown - _no_ansi: bool = False - # When the most recently requested api_token will expire _token_expiration_date: str | None = None @@ -60,7 +54,7 @@ def wrapper(self: "CLIContext", *args: Any, **kwargs: Any) -> Any: fn(self, *args, **kwargs) value = args[0] - print.verbose(f"Storing {fn.__name__}={value} in the config file") + print(f"Storing {fn.__name__}={value} in the config file") self._config_file[fn.__name__] = value with open(self._config_file_path, "w") as f: @@ -100,24 +94,6 @@ def default_host(self) -> str | None: def default_host(self, new_default_host: str | None) -> None: self._default_host = new_default_host - @property - def verbose(self) -> bool: - """Enable extra output.""" - return self._verbose - - @verbose.setter - def verbose(self, new_verbose: bool) -> None: - self._verbose = new_verbose - - @property - def no_ansi(self) -> bool: - """If set removes colorized output.""" - return self._no_ansi - - @no_ansi.setter - def no_ansi(self, new_no_ansi: bool) -> None: - self._no_ansi = new_no_ansi - @property def token_expiration_date(self) -> str | None: """When the most recently requested api_token will expire.""" diff --git a/canvas_cli/utils/print/print.py b/canvas_cli/utils/print/print.py index 775d7abb..aa1fe0c6 100644 --- a/canvas_cli/utils/print/print.py +++ b/canvas_cli/utils/print/print.py @@ -3,7 +3,6 @@ from typing import Any from requests import Response -from rich import print as rich_print class Printer: @@ -39,22 +38,9 @@ def json(message: str | list[str] | dict | None, success: bool = True, **kwargs: Printer._default_print(json.dumps(status)) - @staticmethod - def verbose(*args: Any) -> None: - """Print text only if `verbose` is set in context.""" - from canvas_cli.utils.context import context - - if context.verbose: - Printer._default_print(*args) - @staticmethod def _default_print(*args: Any) -> None: - from canvas_cli.utils.context import context - - if context.no_ansi: - builtin_print(*args) - else: - rich_print(*args) + builtin_print(*args) print = Printer() diff --git a/logger/logger.py b/logger/logger.py index e5e5dbac..1f3280e7 100644 --- a/logger/logger.py +++ b/logger/logger.py @@ -3,8 +3,9 @@ from pubsub.pubsub import Publisher + class PubSubLogHandler(logging.Handler): - def __init__(self)-> None: + def __init__(self) -> None: self.publisher = Publisher() logging.Handler.__init__(self=self) @@ -17,7 +18,7 @@ class PluginLogger: def __init__(self) -> None: self.logger = logging.getLogger("plugin_runner_logger") self.logger.setLevel(logging.INFO) - formatter = logging.Formatter('%(levelname)s %(asctime)s %(message)s') + formatter = logging.Formatter("%(levelname)s %(asctime)s %(message)s") streaming_handler = logging.StreamHandler() streaming_handler.setFormatter(formatter) @@ -31,7 +32,7 @@ def __init__(self) -> None: def debug(self, message) -> None: self.logger.debug(message) - def info(self,message) -> None: + def info(self, message) -> None: self.logger.info(message) def warning(self, message) -> None: diff --git a/plugin_runner/plugin_runner.py b/plugin_runner/plugin_runner.py index 12d772cd..33bb5639 100644 --- a/plugin_runner/plugin_runner.py +++ b/plugin_runner/plugin_runner.py @@ -131,31 +131,39 @@ def load_or_reload_plugin(path: pathlib.Path) -> None: for protocol in protocols: # TODO add class colon validation to existing schema validation - protocol_module, protocol_class = protocol["class"].split(":") - name_and_class = f"{name}:{protocol_module}:{protocol_class}" - - if name_and_class in LOADED_PLUGINS: - log.info(f"Reloading plugin: {name_and_class}") - - result = sandbox_from_module_name(protocol_module) + # TODO when we encounter an exception here, disable the plugin in response + try: + protocol_module, protocol_class = protocol["class"].split(":") + name_and_class = f"{name}:{protocol_module}:{protocol_class}" + except ValueError: + log.error(f"Unable to parse class for plugin '{name}': '{protocol['class']}'") + continue - LOADED_PLUGINS[name_and_class]["active"] = True + try: + if name_and_class in LOADED_PLUGINS: + log.info(f"Reloading plugin '{name_and_class}'") - LOADED_PLUGINS[name_and_class]["class"] = result[protocol_class] - LOADED_PLUGINS[name_and_class]["sandbox"] = result - LOADED_PLUGINS[name_and_class]["secrets"] = secrets_json - else: - log.info(f"Loading plugin: {name_and_class}") + result = sandbox_from_module_name(protocol_module) - result = sandbox_from_module_name(protocol_module) + LOADED_PLUGINS[name_and_class]["active"] = True - LOADED_PLUGINS[name_and_class] = { - "active": True, - "class": result[protocol_class], - "sandbox": result, - "protocol": protocol, - "secrets": secrets_json, - } + LOADED_PLUGINS[name_and_class]["class"] = result[protocol_class] + LOADED_PLUGINS[name_and_class]["sandbox"] = result + LOADED_PLUGINS[name_and_class]["secrets"] = secrets_json + else: + log.info(f"Loading plugin '{name_and_class}'") + + result = sandbox_from_module_name(protocol_module) + + LOADED_PLUGINS[name_and_class] = { + "active": True, + "class": result[protocol_class], + "sandbox": result, + "protocol": protocol, + "secrets": secrets_json, + } + except ImportError as err: + log.error(f"Error importing module '{name_and_class}': {err}") def refresh_event_type_map(): diff --git a/plugin_runner/plugin_synchronizer.py b/plugin_runner/plugin_synchronizer.py index 26a9cd02..ce308c7f 100755 --- a/plugin_runner/plugin_synchronizer.py +++ b/plugin_runner/plugin_synchronizer.py @@ -10,6 +10,7 @@ APP_NAME = os.getenv("APP_NAME") +# TODO: use CUSTOMER_IDENTIFIER here PLUGINS_PUBSUB_CHANNEL = os.getenv("PLUGINS_PUBSUB_CHANNEL", default="plugins") REDIS_ENDPOINT = os.getenv("REDIS_ENDPOINT", f"redis://{APP_NAME}-redis:6379") diff --git a/plugin_runner/sandbox.py b/plugin_runner/sandbox.py index ce66dd39..d530a8ad 100644 --- a/plugin_runner/sandbox.py +++ b/plugin_runner/sandbox.py @@ -36,10 +36,13 @@ "arrow", "base64", "cached_property", + "canvas_sdk.commands", + "canvas_sdk.data", "canvas_sdk.effects", "canvas_sdk.events", "canvas_sdk.protocols", - "canvas_workflow_kit", + "canvas_sdk.utils", + "canvas_sdk.views", "contextlib", "datetime", "dateutil", @@ -62,7 +65,6 @@ "typing", "urllib", "uuid", - "workflow_sdk_loader.builtin_cqms", ] ) diff --git a/pyproject.toml b/pyproject.toml index f06cffcf..a7aefbf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ license = "MIT" name = "canvas" packages = [{include = "canvas_cli"}, {include = "canvas_sdk"}] readme = "README.md" -version = "0.1.7" +version = "0.1.10" [tool.poetry.dependencies] cookiecutter = "*" @@ -48,12 +48,12 @@ keyring = "*" pydantic = "^2.6.1" python = ">=3.11,<3.13" python-dotenv = "^1.0.1" +redis = "^5.0.4" requests = "*" restrictedpython = "^7.1" statsd = "^4.0.1" typer = {extras = ["all"], version = "*"} websocket-client = "^1.7.0" -redis = "^5.0.4" [tool.poetry.group.dev.dependencies] grpcio-tools = "^1.60.1"