Skip to content

Commit 8145be5

Browse files
authored
Low-code CDK: safe get response.json (#18931)
* low-code cdk: safe get response.json * flake fix
1 parent bf58536 commit 8145be5

File tree

7 files changed

+46
-13
lines changed

7 files changed

+46
-13
lines changed

airbyte-cdk/python/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## 0.5.4
4+
Low-code: Get response.json in a safe way
5+
36
## 0.5.3
47
Low-code: Replace EmptySchemaLoader with DefaultSchemaLoader to retain backwards compatibility
58
Low-code: Evaluate backoff strategies at runtime

airbyte-cdk/python/airbyte_cdk/sources/declarative/decoders/json_decoder.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@ class JsonDecoder(Decoder, JsonSchemaMixin):
1919
options: InitVar[Mapping[str, Any]]
2020

2121
def decode(self, response: requests.Response) -> Union[Mapping[str, Any], List]:
22-
return response.json() or {}
22+
try:
23+
return response.json()
24+
except requests.exceptions.JSONDecodeError:
25+
return {}

airbyte-cdk/python/airbyte_cdk/sources/declarative/requesters/error_handlers/http_response_filter.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,23 @@ def _matches_filter(self, response: requests.Response) -> Optional[ResponseActio
7676
else:
7777
return None
7878

79+
@staticmethod
80+
def _safe_response_json(response: requests.Response) -> dict:
81+
try:
82+
return response.json()
83+
except requests.exceptions.JSONDecodeError:
84+
return {}
85+
7986
def _create_error_message(self, response: requests.Response) -> str:
8087
"""
8188
Construct an error message based on the specified message template of the filter.
8289
:param response: The HTTP response which can be used during interpolation
8390
:return: The evaluated error message string to be emitted
8491
"""
85-
return self.error_message.eval(self.config, response=response.json(), headers=response.headers)
92+
return self.error_message.eval(self.config, response=self._safe_response_json(response), headers=response.headers)
8693

8794
def _response_matches_predicate(self, response: requests.Response) -> bool:
88-
return self.predicate and self.predicate.eval(None, response=response.json(), headers=response.headers)
95+
return self.predicate and self.predicate.eval(None, response=self._safe_response_json(response), headers=response.headers)
8996

9097
def _response_contains_error_message(self, response: requests.Response) -> bool:
9198
if not self.error_message_contains:

airbyte-cdk/python/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
setup(
1717
name="airbyte-cdk",
18-
version="0.5.3",
18+
version="0.5.4",
1919
description="A framework for writing Airbyte Connectors.",
2020
long_description=README,
2121
long_description_content_type="text/markdown",

airbyte-cdk/python/unit_tests/sources/declarative/decoders/__init__.py

Whitespace-only changes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#
2+
# Copyright (c) 2022 Airbyte, Inc., all rights reserved.
3+
#
4+
5+
import pytest
6+
import requests
7+
from airbyte_cdk.sources.declarative.decoders.json_decoder import JsonDecoder
8+
9+
10+
@pytest.mark.parametrize(
11+
"response_body, expected_json", (("", {}), ('{"healthcheck": {"status": "ok"}}', {"healthcheck": {"status": "ok"}}))
12+
)
13+
def test_json_decoder(requests_mock, response_body, expected_json):
14+
requests_mock.register_uri("GET", "https://airbyte.io/", text=response_body)
15+
response = requests.get("https://airbyte.io/")
16+
assert JsonDecoder(options={}).decode(response) == expected_json

airbyte-cdk/python/unit_tests/sources/declarative/requesters/error_handlers/test_http_response_filter.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
# Copyright (c) 2022 Airbyte, Inc., all rights reserved.
33
#
44

5-
from unittest.mock import MagicMock
5+
import json
66

77
import pytest
8+
import requests
89
from airbyte_cdk.sources.declarative.requesters.error_handlers import HttpResponseFilter
910
from airbyte_cdk.sources.declarative.requesters.error_handlers.response_action import ResponseAction
1011
from airbyte_cdk.sources.declarative.requesters.error_handlers.response_status import ResponseStatus
@@ -86,18 +87,21 @@
8687
"",
8788
None,
8889
"",
89-
{"status_code": 403, "headers": {"error", "authentication_error"}, "json": {"reason": "permission denied"}},
90+
{"status_code": 403, "headers": {"error": "authentication_error"}, "json": {"reason": "permission denied"}},
9091
None,
9192
id="test_response_does_not_match_filter",
9293
),
9394
],
9495
)
95-
def test_matches(action, http_codes, predicate, error_contains, back_off, error_message, response, expected_response_status):
96-
mock_response = MagicMock()
97-
mock_response.status_code = response.get("status_code")
98-
mock_response.headers = response.get("headers")
99-
mock_response.json.return_value = response.get("json")
100-
96+
def test_matches(requests_mock, action, http_codes, predicate, error_contains, back_off, error_message, response, expected_response_status):
97+
requests_mock.register_uri(
98+
"GET",
99+
"https://airbyte.io/",
100+
text=response.get("json") and json.dumps(response.get("json")),
101+
headers=response.get("headers") or {},
102+
status_code=response.get("status_code"),
103+
)
104+
response = requests.get("https://airbyte.io/")
101105
response_filter = HttpResponseFilter(
102106
action=action,
103107
config={},
@@ -108,7 +112,7 @@ def test_matches(action, http_codes, predicate, error_contains, back_off, error_
108112
error_message=error_message,
109113
)
110114

111-
actual_response_status = response_filter.matches(mock_response, backoff_time=back_off or 10)
115+
actual_response_status = response_filter.matches(response, backoff_time=back_off or 10)
112116
if expected_response_status:
113117
assert actual_response_status.action == expected_response_status.action
114118
assert actual_response_status.retry_in == expected_response_status.retry_in

0 commit comments

Comments
 (0)