diff --git a/conftest.py b/conftest.py index fe6acfa..87755a2 100644 --- a/conftest.py +++ b/conftest.py @@ -1,7 +1,8 @@ -import pytest import time from typing import Any +import pytest + @pytest.fixture def sleepless(monkeypatch: Any) -> None: diff --git a/src/connector_http/commands/get_request.py b/src/connector_http/commands/get_request.py index a18391c..7759a6f 100644 --- a/src/connector_http/commands/get_request.py +++ b/src/connector_http/commands/get_request.py @@ -1,8 +1,12 @@ -import requests +from typing import Any + +import requests # type: ignore +from spiffworkflow_connector_command.command_interface import CommandResultDictV1 +from spiffworkflow_connector_command.command_interface import ConnectorCommand -class GetRequest: +class GetRequest(ConnectorCommand): def __init__(self, url: str, headers: dict[str, str] | None = None, @@ -16,7 +20,7 @@ class GetRequest: self.basic_auth_username = basic_auth_username self.basic_auth_password = basic_auth_password - def execute(self, config, task_data): + def execute(self, _config: Any, _task_data: Any) -> CommandResultDictV1: auth = None if self.basic_auth_username is not None and self.basic_auth_password is not None: auth = (self.basic_auth_username, self.basic_auth_password) diff --git a/src/connector_http/commands/get_request_v2.py b/src/connector_http/commands/get_request_v2.py index aa25896..c7e4d08 100644 --- a/src/connector_http/commands/get_request_v2.py +++ b/src/connector_http/commands/get_request_v2.py @@ -1,118 +1,20 @@ -import json -import time from typing import Any import requests # type: ignore -from spiffworkflow_connector_command.command_interface import CommandErrorDict -from spiffworkflow_connector_command.command_interface import CommandResponseDict -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 connector_http.http_request_base import HttpRequestBase -class GetRequestV2(ConnectorCommand): + +class GetRequestV2(ConnectorCommand, HttpRequestBase): def __init__(self, - url: str, - headers: dict[str, str] | None = None, - params: dict[str, str] | None = None, - basic_auth_username: str | None = None, - basic_auth_password: str | None = None, - attempts: int | None = None, + attempts: int | None = None, **kwargs: Any ): - self.url = url - self.headers = headers or {} - self.params = params or {} - self.basic_auth_username = basic_auth_username - self.basic_auth_password = basic_auth_password - + HttpRequestBase.__init__(self, **kwargs) if not isinstance(attempts, int) or attempts < 1 or attempts > 10: attempts = 1 - self.attempts = attempts - def execute(self, _config: Any, _task_data: dict) -> CommandResultDict: - logs = [] - - def log(msg: str) -> None: - print(f"LOG: {msg}") - logs.append(f"[{time.time()}] {msg}") - - log("Will execute") - - auth = None - if self.basic_auth_username is not None and self.basic_auth_password is not None: - auth = (self.basic_auth_username, self.basic_auth_password) - log("Set auth") - - attempt = 1 - command_response: dict = {} - error: CommandErrorDict | None = None - status = 0 - mimetype = "application/json" - http_response = None - while attempt <= self.attempts: - command_response = {} - status = 0 - if attempt > 1: - log("Sleeping before next attempt") - time.sleep(1) - - log(f"Will attempt {attempt} of {self.attempts}") - http_response = None - - try: - log(f"Will call {self.url}") - http_response = requests.get(self.url, self.params, headers=self.headers, auth=auth, timeout=300) - log(f"Did call {self.url}") - - log("Will parse http_response") - status = http_response.status_code - except Exception as e: - log(f"Did catch exception: {e}") - error = self.create_error_from_exception(exception=e, http_response=http_response) - if status < 300: - status = 500 - finally: - log(f"Did attempt {attempt} of {self.attempts}") - - # check for 500 level status - if status // 100 != 5: - break - - attempt += 1 - - log("Did execute") - - if http_response is not None: - command_response = {"raw_response": http_response.text} - # this string can include modifiers like UTF-8, which is why it's not using == - if 'application/json' in http_response.headers.get('Content-Type', ''): - try: - command_response = json.loads(http_response.text) - except Exception as e: - error = self.create_error_from_exception(exception=e, http_response=http_response) - log("Did parse http_response") - - if status >= 400 and error is None: - error = self.create_error(error_name=f"HttpError{status}", http_response=http_response) - - return_response: CommandResponseDict = { - "command_response": command_response, - "spiff__logs": logs, - "error": error, - } - result: CommandResultDict = { - "response": return_response, - "status": status, - "mimetype": mimetype, - } - - return result - - def create_error_from_exception(self, exception: Exception, http_response: requests.Response | None) -> CommandErrorDict: - return self.create_error(error_name=exception.__class__.__name__, http_response=http_response, additional_message=str(exception)) - - def create_error(self, error_name: str, http_response: requests.Response | None, additional_message: str = "") -> CommandErrorDict: - raw_response = http_response.text if http_response is not None else None - message = f"Received Error: {additional_message}. Raw http_response was: {raw_response}" - error: CommandErrorDict = {"error_name": error_name, "message": message} - return error + def execute(self, _config: Any, _task_data: dict) -> CommandResultDictV2: + return self.run_request(requests.get) diff --git a/src/connector_http/commands/post_request.py b/src/connector_http/commands/post_request.py index 9b92e1b..f49de84 100644 --- a/src/connector_http/commands/post_request.py +++ b/src/connector_http/commands/post_request.py @@ -1,9 +1,11 @@ from typing import Any -import requests +import requests # type: ignore +from spiffworkflow_connector_command.command_interface import CommandResultDictV1 +from spiffworkflow_connector_command.command_interface import ConnectorCommand -class PostRequest: +class PostRequest(ConnectorCommand): def __init__(self, url: str, headers: dict[str, str] | None, @@ -17,7 +19,7 @@ class PostRequest: self.basic_auth_password = basic_auth_password self.data = data - def execute(self, config, task_data): + def execute(self, _config: Any, _task_data: Any) -> CommandResultDictV1: auth = None if self.basic_auth_username is not None and self.basic_auth_password is not None: auth = (self.basic_auth_username, self.basic_auth_password) diff --git a/src/connector_http/commands/post_request_v2.py b/src/connector_http/commands/post_request_v2.py index 315f34e..2534fe1 100644 --- a/src/connector_http/commands/post_request_v2.py +++ b/src/connector_http/commands/post_request_v2.py @@ -1,66 +1,12 @@ -import json -import time from typing import Any -import requests +import requests # type: ignore +from spiffworkflow_connector_command.command_interface import CommandResultDictV2 +from spiffworkflow_connector_command.command_interface import ConnectorCommand + +from connector_http.http_request_base import HttpRequestBase -class PostRequestV2: - def __init__(self, - url: str, - headers: dict[str, str] | None = None, - basic_auth_username: str | None = None, - basic_auth_password: str | None = None, - data: dict[str, Any] | None = None, - ): - self.url = url - self.headers = headers or {} - self.basic_auth_username = basic_auth_username - self.basic_auth_password = basic_auth_password - self.data = data - - def execute(self, config, task_data): - logs = [] - - def log(msg): - logs.append(f"[{time.time()}] {msg}") - - response = {} - status = 0 - mimetype = "application/json" - - log("Will execute") - - auth = None - if self.basic_auth_username is not None and self.basic_auth_password is not None: - auth = (self.basic_auth_username, self.basic_auth_password) - log("basic auth has been set") - - try: - log(f"Will call {self.url} with data {self.data}") - api_response = requests.post(self.url, headers=self.headers, auth=auth, json=self.data, timeout=300) - log(f"Did call {self.url}") - - log(f"Will parse response with status code {api_response.status_code}") - status = api_response.status_code - response = json.loads(api_response.text) if api_response.text else {} - log("Did parse response") - except Exception as e: - log(f"Did catch exception: {e}") - if len(response) == 0: - response = f'{"error": {e}, "raw_response": {api_response.text}}', - if status == 0: - status = 500 - finally: - log("Did execute") - - result = { - "response": { - "api_response": response, - "spiff__logs": logs, - }, - "status": status, - "mimetype": mimetype, - } - - return result +class PostRequestV2(ConnectorCommand, HttpRequestBase): + def execute(self, _config: Any, _task_data: dict) -> CommandResultDictV2: + return self.run_request(requests.post) diff --git a/src/connector_http/http_request_base.py b/src/connector_http/http_request_base.py new file mode 100644 index 0000000..c9edf67 --- /dev/null +++ b/src/connector_http/http_request_base.py @@ -0,0 +1,116 @@ +import json +import time +from collections.abc import Callable + +import requests # type: ignore +from spiffworkflow_connector_command.command_interface import CommandErrorDict +from spiffworkflow_connector_command.command_interface import CommandResultDictV2 +from spiffworkflow_connector_command.command_interface import ConnectorProxyResponseDict + + +class HttpRequestBase: + def __init__(self, + url: str, + headers: dict[str, str] | None = None, + params: dict[str, str] | None = None, + basic_auth_username: str | None = None, + basic_auth_password: str | None = None, + ): + self.url = url + self.headers = headers or {} + self.params = params or {} + self.basic_auth_username = basic_auth_username + self.basic_auth_password = basic_auth_password + self.attempts = 1 + + def _create_error_from_exception(self, exception: Exception, http_response: requests.Response | None) -> CommandErrorDict: + return self._create_error( + error_name=exception.__class__.__name__, http_response=http_response, additional_message=str(exception) + ) + + def _create_error( + self, error_name: str, http_response: requests.Response | None, additional_message: str = "" + ) -> CommandErrorDict: + raw_response = http_response.text if http_response is not None else None + message = f"Received Error: {additional_message}. Raw http_response was: {raw_response}" + error: CommandErrorDict = {"error_name": error_name, "message": message} + return error + + def run_request(self, request_function: Callable) -> CommandResultDictV2: + logs = [] + + def log(msg: str) -> None: + print(f"LOG: {msg}") + logs.append(f"[{time.time()}] {msg}") + + log("Will execute") + + auth = None + if self.basic_auth_username is not None and self.basic_auth_password is not None: + auth = (self.basic_auth_username, self.basic_auth_password) + log("Set auth") + + attempt = 1 + command_response: dict = {} + error: CommandErrorDict | None = None + status = 0 + mimetype = "application/json" + http_response = None + while attempt <= self.attempts: + command_response = {} + status = 0 + if attempt > 1: + log("Sleeping before next attempt") + time.sleep(1) + + log(f"Will attempt {attempt} of {self.attempts}") + http_response = None + + try: + log(f"Will call {self.url}") + http_response = request_function(self.url, self.params, headers=self.headers, auth=auth, timeout=300) + log(f"Did call {self.url}") + + log("Will parse http_response") + status = http_response.status_code + except Exception as e: + log(f"Did catch exception: {e}") + error = self._create_error_from_exception(exception=e, http_response=http_response) + if status < 300: + status = 500 + finally: + log(f"Did attempt {attempt} of {self.attempts}") + + # check for 500 level status + if status // 100 != 5: + break + + attempt += 1 + + log("Did execute") + + if http_response is not None: + command_response = {"raw_response": http_response.text} + # this string can include modifiers like UTF-8, which is why it's not using == + if 'application/json' in http_response.headers.get('Content-Type', ''): + try: + command_response = json.loads(http_response.text) + except Exception as e: + error = self._create_error_from_exception(exception=e, http_response=http_response) + log("Did parse http_response") + + if status >= 400 and error is None: + error = self._create_error(error_name=f"HttpError{status}", http_response=http_response) + + return_response: ConnectorProxyResponseDict = { + "command_response": command_response, + "spiff__logs": logs, + "error": error, + } + result: CommandResultDictV2 = { + "response": return_response, + "status": status, + "mimetype": mimetype, + } + + return result diff --git a/tests/connector_http/unit/test_get_request_v2.py b/tests/connector_http/unit/test_get_request_v2.py index 3652078..3d28bf2 100644 --- a/tests/connector_http/unit/test_get_request_v2.py +++ b/tests/connector_http/unit/test_get_request_v2.py @@ -1,20 +1,20 @@ -from unittest.mock import patch -from typing import Any import json +from typing import Any +from unittest.mock import patch from connector_http.commands.get_request_v2 import GetRequestV2 class TestGetRequestV2: def test_html_from_url(self) -> None: - get_requestor = GetRequestV2(url="http://example.com") + request = GetRequestV2(url="http://example.com") result = None return_html = "Hey" with patch("requests.get") as mock_request: mock_request.return_value.status_code = 200 mock_request.return_value.ok = True mock_request.return_value.text = return_html - result = get_requestor.execute(None, {}) + result = request.execute(None, {}) assert result is not None assert result["status"] == 200 assert result["mimetype"] == "application/json" @@ -27,7 +27,7 @@ class TestGetRequestV2: assert len(response["spiff__logs"]) > 0 def test_json_from_url(self) -> None: - get_requestor = GetRequestV2(url="http://example.com") + request = GetRequestV2(url="http://example.com") result = None return_json = {"hey": "we_return"} with patch("requests.get") as mock_request: @@ -35,7 +35,7 @@ class TestGetRequestV2: mock_request.return_value.ok = True mock_request.return_value.headers = {"Content-Type": "application/json"} mock_request.return_value.text = json.dumps(return_json) - result = get_requestor.execute(None, {}) + result = request.execute(None, {}) assert result is not None assert result["status"] == 200 assert result["mimetype"] == "application/json" @@ -48,14 +48,14 @@ class TestGetRequestV2: assert len(response["spiff__logs"]) > 0 def test_can_handle_500(self, sleepless: Any) -> None: - get_requestor = GetRequestV2(url="http://example.com", attempts=3) + request = GetRequestV2(url="http://example.com", attempts=3) result = None return_json = {"error": "we_did_error"} with patch("requests.get") as mock_request: mock_request.return_value.status_code = 500 mock_request.return_value.headers = {"Content-Type": "application/json"} mock_request.return_value.text = json.dumps(return_json) - result = get_requestor.execute(None, {}) + result = request.execute(None, {}) assert mock_request.call_count == 3 assert result is not None assert result["status"] == 500 diff --git a/tests/connector_http/unit/test_post_request_v2.py b/tests/connector_http/unit/test_post_request_v2.py new file mode 100644 index 0000000..82791e5 --- /dev/null +++ b/tests/connector_http/unit/test_post_request_v2.py @@ -0,0 +1,71 @@ +import json +from typing import Any +from unittest.mock import patch + +from connector_http.commands.post_request_v2 import PostRequestV2 + + +class TestPostRequestV2: + def test_html_from_url(self) -> None: + request = PostRequestV2(url="http://example.com") + result = None + return_html = "Hey" + with patch("requests.post") as mock_request: + mock_request.return_value.status_code = 200 + mock_request.return_value.ok = True + mock_request.return_value.text = return_html + result = request.execute(None, {}) + assert mock_request.call_count == 1 + assert result is not None + assert result["status"] == 200 + assert result["mimetype"] == "application/json" + + response = result["response"] + assert response is not None + assert response["command_response"] == {"raw_response": return_html} + assert response["error"] is None + assert response["spiff__logs"] is not None + assert len(response["spiff__logs"]) > 0 + + def test_json_from_url(self) -> None: + request = PostRequestV2(url="http://example.com") + result = None + return_json = {"hey": "we_return"} + with patch("requests.post") as mock_request: + mock_request.return_value.status_code = 200 + mock_request.return_value.ok = True + mock_request.return_value.headers = {"Content-Type": "application/json"} + mock_request.return_value.text = json.dumps(return_json) + result = request.execute(None, {}) + assert mock_request.call_count == 1 + assert result is not None + assert result["status"] == 200 + assert result["mimetype"] == "application/json" + + response = result["response"] + assert response is not None + assert response["command_response"] == return_json + assert response["error"] is None + assert response["spiff__logs"] is not None + assert len(response["spiff__logs"]) > 0 + + def test_can_handle_500(self, sleepless: Any) -> None: + request = PostRequestV2(url="http://example.com") + result = None + return_json = {"error": "we_did_error"} + with patch("requests.post") as mock_request: + mock_request.return_value.status_code = 500 + mock_request.return_value.headers = {"Content-Type": "application/json"} + mock_request.return_value.text = json.dumps(return_json) + result = request.execute(None, {}) + assert mock_request.call_count == 1 + assert result is not None + assert result["status"] == 500 + assert result["mimetype"] == "application/json" + + response = result["response"] + assert response is not None + assert response["command_response"] == return_json + assert response["error"] is not None + assert response["spiff__logs"] is not None + assert len(response["spiff__logs"]) > 0