From dcd50ed7628502fcd71ac2b342259d7cad80c89a Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 13 Oct 2023 11:57:47 -0400 Subject: [PATCH 1/4] WIP for error boundary support w/ burnettk --- pyproject.toml | 3 ++ src/spiffworkflow_proxy/blueprint.py | 43 +++++++++++++------ src/spiffworkflow_proxy/plugin_service.py | 26 +++++++---- .../commands/combine_strings.py | 24 +++++++++-- 4 files changed, 69 insertions(+), 27 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index af11721..7bf37c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,9 @@ python = "^3.9" Flask = "^2.2" Flask-OAuthlib = "^0.9.6" +# spiffworkflow-connector-command = {git = "https://github.com/sartography/spiffworkflow-connector-command.git", rev = "main"} +spiffworkflow-connector-command = {develop = true, path = "../spiffworkflow-connector-command"} + [tool.poetry.group.dev.dependencies] pytest = "^7.2.0" connector-example = {path = "tests/mock_connectors/connector-example", develop = true} diff --git a/src/spiffworkflow_proxy/blueprint.py b/src/spiffworkflow_proxy/blueprint.py index 7b78fd0..56e1818 100644 --- a/src/spiffworkflow_proxy/blueprint.py +++ b/src/spiffworkflow_proxy/blueprint.py @@ -11,6 +11,7 @@ from flask import session from flask import url_for from flask_oauthlib.contrib.client import OAuth # type: ignore +from spiffworkflow_connector_command.command_interface import CommandResponseDict from spiffworkflow_proxy.plugin_service import PluginService @@ -35,10 +36,15 @@ def list_commands() -> Response: @proxy_blueprint.route("/v1/do//", methods=["GET", "POST"]) def do_command(plugin_display_name: str, command_name: str) -> Response: + operator_id = f"{plugin_display_name}/{command_name}" + # import pdb; pdb.set_trace() command = PluginService.command_named(plugin_display_name, command_name) if command is None: return json_error_response( - f"Command not found: {plugin_display_name}:{command_name}", status=404 + message="It either does not exist or does not inherit from spiffworkflow_connector_command.", + error_name="command_not_found", + operator_id=operator_id, + status=404 ) params = typing.cast(dict, request.json) @@ -48,17 +54,19 @@ def do_command(plugin_display_name: str, command_name: str) -> Response: result = command(**params).execute(current_app.config, task_data) except Exception as e: return json_error_response( - f"Error encountered when executing {plugin_display_name}:{command_name} {str(e)}", - status=404, + message=str(e), + error_name=e.__class__.__name__, + operator_id=operator_id, + status=500 ) - if 'status' in result: - status_code = int(result['status']) - else: - status_code = 200 - if isinstance(result["response"], dict): - response = json.dumps(result["response"]) - else: - response = result["response"] + + status_code = int(result['status']) + return_response = result["response"] + if "operator_id" not in return_response or return_response["operator_id"] is None: + return_response["operator_id"] = operator_id + print(f"return_response: {return_response}") + response = json.dumps(return_response) + print(f"result: {result}") return Response(response, mimetype=result["mimetype"], status=status_code) @@ -142,7 +150,14 @@ def tokensaver(token: str) -> None: return handler -def json_error_response(message: str, status: int) -> Response: - resp = {"error": message, "status": status} - return Response(json.dumps(resp), status=status) +def json_error_response(message: str, operator_id: str, error_name: str, status: int) -> Response: + response: CommandResponseDict = { + "api_response": {}, + "error": { + "message": message, + "error_name": error_name, + }, + "operator_id": operator_id, + } + return Response(json.dumps(response), status=status) diff --git a/src/spiffworkflow_proxy/plugin_service.py b/src/spiffworkflow_proxy/plugin_service.py index 0d8d0a4..3efe0fa 100644 --- a/src/spiffworkflow_proxy/plugin_service.py +++ b/src/spiffworkflow_proxy/plugin_service.py @@ -7,6 +7,8 @@ from inspect import Parameter from typing import Any +from spiffworkflow_connector_command.command_interface import ConnectorCommand + class PluginService: """ @@ -33,7 +35,7 @@ def available_plugins() -> dict[str, types.ModuleType]: } @staticmethod - def available_auths_by_plugin() -> dict[str, dict[str, type]]: + def available_auths_by_plugin() -> dict[str, dict[str, type[ConnectorCommand]]]: return { plugin_name: dict(PluginService.auths_for_plugin( plugin_name, plugin @@ -42,7 +44,7 @@ def available_auths_by_plugin() -> dict[str, dict[str, type]]: } @staticmethod - def available_commands_by_plugin() -> dict[str, dict[str, type]]: + def available_commands_by_plugin() -> dict[str, dict[str, type[ConnectorCommand]]]: return { plugin_name: dict(PluginService.commands_for_plugin( plugin_name, plugin @@ -56,7 +58,7 @@ def target_id(plugin_name: str, target_name: str) -> str: return f"{plugin_display_name}/{target_name}" @staticmethod - def auth_named(plugin_display_name: str, auth_name: str) -> type | None: + def auth_named(plugin_display_name: str, auth_name: str) -> type[ConnectorCommand] | None: plugin_name = PluginService.plugin_name_from_display_name(plugin_display_name) available_auths_by_plugin = PluginService.available_auths_by_plugin() @@ -66,7 +68,7 @@ def auth_named(plugin_display_name: str, auth_name: str) -> type | None: return None @staticmethod - def command_named(plugin_display_name: str, command_name: str) -> type | None: + def command_named(plugin_display_name: str, command_name: str) -> type[ConnectorCommand] | None: plugin_name = PluginService.plugin_name_from_display_name(plugin_display_name) available_commands_by_plugin = PluginService.available_commands_by_plugin() @@ -95,21 +97,23 @@ def modules_for_plugin_in_package( @staticmethod def targets_for_plugin( plugin_name: str, plugin: types.ModuleType, target_package_name: str - ) -> Generator[tuple[str, type], None, None]: + ) -> Generator[tuple[str, type[ConnectorCommand]], None, None]: for module_name, module in PluginService.modules_for_plugin_in_package( plugin, target_package_name ): - for member_name, member in inspect.getmembers(module, inspect.isclass): + # for member_name, member in inspect.getmembers(module, inspect.isclass): + for member_name, member in inspect.getmembers(module, PluginService.is_connector_command): if member.__module__ == module_name: yield member_name, member @staticmethod - def auths_for_plugin(plugin_name: str, plugin: types.ModuleType) -> Generator[tuple[str, type], None, None]: + def auths_for_plugin(plugin_name: str, plugin: types.ModuleType) -> Generator[tuple[str, type[ConnectorCommand]], None, None]: yield from PluginService.targets_for_plugin(plugin_name, plugin, "auths") @staticmethod - def commands_for_plugin(plugin_name: str, plugin: types.ModuleType) -> Generator[tuple[str, type], None, None]: - # TODO check if class has an execute method before yielding + def commands_for_plugin( + plugin_name: str, plugin: types.ModuleType + ) -> Generator[tuple[str, type[ConnectorCommand]], None, None]: yield from PluginService.targets_for_plugin(plugin_name, plugin, "commands") @staticmethod @@ -165,3 +169,7 @@ def describe_target(plugin_name: str, target_name: str, target: type) -> dict: parameters = PluginService.callable_params_desc(target.__init__) # type: ignore target_id = PluginService.target_id(plugin_name, target_name) return {"id": target_id, "parameters": parameters} + + @staticmethod + def is_connector_command(module: Any) -> bool: + return inspect.isclass(module) and issubclass(module, ConnectorCommand) diff --git a/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py b/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py index fb85b95..8400419 100644 --- a/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py +++ b/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py @@ -1,8 +1,12 @@ """Simple Example Command.""" from typing import Any +from spiffworkflow_connector_command.command_interface import CommandResponseDict +from spiffworkflow_connector_command.command_interface import CommandResultDict +from spiffworkflow_connector_command.command_interface import ConnectorCommand -class CombineStrings: + +class CombineStrings(ConnectorCommand): """Takes two strings, combines them together, and returns a single string! AMAZIN!.""" def __init__( @@ -16,7 +20,19 @@ def __init__( self.arg1 = arg1 self.arg2 = arg2 - def execute(self, config: Any, task_data: Any) -> Any: + def execute(self, config: Any, task_data: Any) -> CommandResultDict: """Execute.""" - # Get the service resource. - return self.arg1 + self.arg2 + + return_response: CommandResponseDict = { + "api_response": {"example_response": "whatever you want", "arg1": self.arg1, "arg2": self.arg2}, + "spiff__logs": [], + "error": None, + } + result: CommandResultDict = { + "response": return_response, + "status": 200, + "mimetype": "application/json", + } + + return result + From 2adfbe96a8e8bdd68ade9420807fa1c8126dd462 Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 13 Oct 2023 12:47:23 -0400 Subject: [PATCH 2/4] updated to ConnectorProxyResponseDict w/ burnettk --- src/spiffworkflow_proxy/blueprint.py | 4 ++-- .../src/connector_example/commands/combine_strings.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spiffworkflow_proxy/blueprint.py b/src/spiffworkflow_proxy/blueprint.py index 56e1818..6151920 100644 --- a/src/spiffworkflow_proxy/blueprint.py +++ b/src/spiffworkflow_proxy/blueprint.py @@ -11,7 +11,7 @@ from flask import session from flask import url_for from flask_oauthlib.contrib.client import OAuth # type: ignore -from spiffworkflow_connector_command.command_interface import CommandResponseDict +from spiffworkflow_connector_command.command_interface import ConnectorProxyResponseDict from spiffworkflow_proxy.plugin_service import PluginService @@ -151,7 +151,7 @@ def tokensaver(token: str) -> None: def json_error_response(message: str, operator_id: str, error_name: str, status: int) -> Response: - response: CommandResponseDict = { + response: ConnectorProxyResponseDict = { "api_response": {}, "error": { "message": message, diff --git a/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py b/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py index 8400419..fe7d3ea 100644 --- a/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py +++ b/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py @@ -1,7 +1,7 @@ """Simple Example Command.""" from typing import Any -from spiffworkflow_connector_command.command_interface import CommandResponseDict +from spiffworkflow_connector_command.command_interface import ConnectorProxyResponseDict from spiffworkflow_connector_command.command_interface import CommandResultDict from spiffworkflow_connector_command.command_interface import ConnectorCommand @@ -23,7 +23,7 @@ def __init__( def execute(self, config: Any, task_data: Any) -> CommandResultDict: """Execute.""" - return_response: CommandResponseDict = { + return_response: ConnectorProxyResponseDict = { "api_response": {"example_response": "whatever you want", "arg1": self.arg1, "arg2": self.arg2}, "spiff__logs": [], "error": None, From a1eb9f20622313f33879244e4cf36d136d78cd35 Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 13 Oct 2023 15:48:45 -0400 Subject: [PATCH 3/4] use error_code instead of error_name and bumped version to 1.0.0 w/ burnettk --- poetry.lock | 21 ++++++++++++++++++++- pyproject.toml | 6 +++--- src/spiffworkflow_proxy/blueprint.py | 17 ++++------------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/poetry.lock b/poetry.lock index aaffd10..9edc806 100644 --- a/poetry.lock +++ b/poetry.lock @@ -549,6 +549,25 @@ files = [ {file = "ruff-0.0.292.tar.gz", hash = "sha256:1093449e37dd1e9b813798f6ad70932b57cf614e5c2b5c51005bf67d55db33ac"}, ] +[[package]] +name = "spiffworkflow-connector-command" +version = "0.2.0" +description = "Make HTTP Requests available to SpiffWorkflow Service Tasks" +optional = false +python-versions = "^3.9" +files = [] +develop = false + +[package.dependencies] +requests = "^2.28.2" +typing-extensions = "^4.8.0" + +[package.source] +type = "git" +url = "https://github.com/sartography/spiffworkflow-connector-command.git" +reference = "main" +resolved_reference = "88bcbafdb1ffbf3f85fa9f13e05e41ace48b0988" + [[package]] name = "tomli" version = "2.0.1" @@ -623,4 +642,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "06dddc026bce82ee3aa42e888ca236f09e68ad1f443468b40d76f50193c57137" +content-hash = "62a02097cb689098c29beaceecf6ec3ac52809f9412161455d085cbd26bc0855" diff --git a/pyproject.toml b/pyproject.toml index 7bf37c4..495ef5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "spiffworkflow-proxy" -version = "0.1.0" +version = "1.0.0" description = "A blueprint that can allow (and limit) SpiffWorkflow's Service Tasks access to an organizations API's, such as connections to AWS Services and existing applications." authors = ["Dan "] readme = "README.md" @@ -11,8 +11,8 @@ python = "^3.9" Flask = "^2.2" Flask-OAuthlib = "^0.9.6" -# spiffworkflow-connector-command = {git = "https://github.com/sartography/spiffworkflow-connector-command.git", rev = "main"} -spiffworkflow-connector-command = {develop = true, path = "../spiffworkflow-connector-command"} +spiffworkflow-connector-command = {git = "https://github.com/sartography/spiffworkflow-connector-command.git", rev = "main"} +# spiffworkflow-connector-command = {develop = true, path = "../spiffworkflow-connector-command"} [tool.poetry.group.dev.dependencies] pytest = "^7.2.0" diff --git a/src/spiffworkflow_proxy/blueprint.py b/src/spiffworkflow_proxy/blueprint.py index 6151920..2d76a5c 100644 --- a/src/spiffworkflow_proxy/blueprint.py +++ b/src/spiffworkflow_proxy/blueprint.py @@ -36,14 +36,11 @@ def list_commands() -> Response: @proxy_blueprint.route("/v1/do//", methods=["GET", "POST"]) def do_command(plugin_display_name: str, command_name: str) -> Response: - operator_id = f"{plugin_display_name}/{command_name}" - # import pdb; pdb.set_trace() command = PluginService.command_named(plugin_display_name, command_name) if command is None: return json_error_response( message="It either does not exist or does not inherit from spiffworkflow_connector_command.", - error_name="command_not_found", - operator_id=operator_id, + error_code="command_not_found", status=404 ) @@ -55,18 +52,13 @@ def do_command(plugin_display_name: str, command_name: str) -> Response: except Exception as e: return json_error_response( message=str(e), - error_name=e.__class__.__name__, - operator_id=operator_id, + error_code=e.__class__.__name__, status=500 ) status_code = int(result['status']) return_response = result["response"] - if "operator_id" not in return_response or return_response["operator_id"] is None: - return_response["operator_id"] = operator_id - print(f"return_response: {return_response}") response = json.dumps(return_response) - print(f"result: {result}") return Response(response, mimetype=result["mimetype"], status=status_code) @@ -150,14 +142,13 @@ def tokensaver(token: str) -> None: return handler -def json_error_response(message: str, operator_id: str, error_name: str, status: int) -> Response: +def json_error_response(message: str, error_code: str, status: int) -> Response: response: ConnectorProxyResponseDict = { "api_response": {}, "error": { "message": message, - "error_name": error_name, + "error_code": error_code, }, - "operator_id": operator_id, } return Response(json.dumps(response), status=status) From 9579ac8a72f41b55b6f22f2a1b9f6219a134b038 Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 13 Oct 2023 15:51:41 -0400 Subject: [PATCH 4/4] fixed linting issues w/ burnettk --- src/spiffworkflow_proxy/blueprint.py | 2 +- .../src/connector_example/commands/combine_strings.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/spiffworkflow_proxy/blueprint.py b/src/spiffworkflow_proxy/blueprint.py index 2d76a5c..b7bcac9 100644 --- a/src/spiffworkflow_proxy/blueprint.py +++ b/src/spiffworkflow_proxy/blueprint.py @@ -144,7 +144,7 @@ def tokensaver(token: str) -> None: def json_error_response(message: str, error_code: str, status: int) -> Response: response: ConnectorProxyResponseDict = { - "api_response": {}, + "command_response": {}, "error": { "message": message, "error_code": error_code, diff --git a/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py b/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py index fe7d3ea..3ad22c7 100644 --- a/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py +++ b/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py @@ -1,9 +1,9 @@ """Simple Example Command.""" from typing import Any -from spiffworkflow_connector_command.command_interface import ConnectorProxyResponseDict -from spiffworkflow_connector_command.command_interface import CommandResultDict +from spiffworkflow_connector_command.command_interface import CommandResultDictV2 from spiffworkflow_connector_command.command_interface import ConnectorCommand +from spiffworkflow_connector_command.command_interface import ConnectorProxyResponseDict class CombineStrings(ConnectorCommand): @@ -20,15 +20,15 @@ def __init__( self.arg1 = arg1 self.arg2 = arg2 - def execute(self, config: Any, task_data: Any) -> CommandResultDict: + def execute(self, config: Any, task_data: Any) -> CommandResultDictV2: """Execute.""" return_response: ConnectorProxyResponseDict = { - "api_response": {"example_response": "whatever you want", "arg1": self.arg1, "arg2": self.arg2}, + "command_response": {"example_response": "whatever you want", "arg1": self.arg1, "arg2": self.arg2}, "spiff__logs": [], "error": None, } - result: CommandResultDict = { + result: CommandResultDictV2 = { "response": return_response, "status": 200, "mimetype": "application/json",