Skip to content

Commit

Permalink
WIP for error boundary support w/ burnettk
Browse files Browse the repository at this point in the history
  • Loading branch information
jasquat committed Oct 13, 2023
1 parent 0024d84 commit 88229a0
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 23 deletions.
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python 3.11.0
12 changes: 12 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import pytest
import time
from typing import Any


@pytest.fixture
def sleepless(monkeypatch: Any) -> None:

def sleep(seconds: int) -> None:
pass

monkeypatch.setattr(time, 'sleep', sleep)
18 changes: 17 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ packages = [{include = "connector_http", from = "src" }]
[tool.poetry.dependencies]
python = "^3.9"
requests = "^2.28.2"
# 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]
Expand Down
74 changes: 52 additions & 22 deletions src/connector_http/commands/get_request_v2.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import json
import time
from typing import Any

import requests
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 ConnectorCommand


class GetRequestV2:
class GetRequestV2(ConnectorCommand):
def __init__(self,
url: str,
headers: dict[str, str] | None = None,
Expand All @@ -24,10 +29,11 @@ def __init__(self,

self.attempts = attempts

def execute(self, config, task_data):
def execute(self, _config: Any, _task_data: dict) -> CommandResultDict:
logs = []

def log(msg):
def log(msg: str) -> None:
print(f"LOG: {msg}")
logs.append(f"[{time.time()}] {msg}")

log("Will execute")
Expand All @@ -38,51 +44,75 @@ def log(msg):
log("Set auth")

attempt = 1

command_response: dict = {}
error: CommandErrorDict | None = None
status = 0
mimetype = "application/json"
http_response = None
while attempt <= self.attempts:
response = {}
command_response = {}
status = 0
mimetype = "application/json"

if attempt > 1:
log("Sleeping before next attempt")
time.sleep(1)

log(f"Will attempt {attempt} of {self.attempts}")
api_response = None
http_response = None

try:
log(f"Will call {self.url}")
api_response = requests.get(self.url, self.params, headers=self.headers, auth=auth, timeout=300)
http_response = requests.get(self.url, self.params, headers=self.headers, auth=auth, timeout=300)
log(f"Did call {self.url}")

log("Will parse response")
status = api_response.status_code
response = json.loads(api_response.text)
log("Did parse response")
log("Will parse http_response")
status = http_response.status_code
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:
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")

result = {
"response": {
"api_response": response,
"spiff__logs": logs,
},
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
69 changes: 69 additions & 0 deletions tests/connector_http/unit/test_get_request_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from unittest.mock import patch
from typing import Any
import json

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")
result = None
return_html = "<html>Hey</html>"
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, {})
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:
get_requestor = GetRequestV2(url="http://example.com")
result = None
return_json = {"hey": "we_return"}
with patch("requests.get") 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 = get_requestor.execute(None, {})
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:
get_requestor = 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, {})
assert mock_request.call_count == 3
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

0 comments on commit 88229a0

Please sign in to comment.