Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion tap_delighted/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ def raise_for_error(response: requests.Response) -> None:
response.status_code, {}
).get("message", "Unknown Error")
message = f"HTTP-error-code: {response.status_code}, Error: {response_json.get('message', error_message)}"

exc = ERROR_CODE_EXCEPTION_MAPPING.get(response.status_code, {}).get(
"raise_exception", DelightedError
"raise_exception", DelightedError
)

# For 5xx errors, use backoff exception if not specifically mapped
if 500 <= response.status_code < 600 and response.status_code not in ERROR_CODE_EXCEPTION_MAPPING.keys():
exc = DelightedBackoffError

raise exc(message, response) from None


Expand Down
18 changes: 0 additions & 18 deletions tap_delighted/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,6 @@ class DelightedInternalServerError(DelightedBackoffError):
pass


class DelightedNotImplementedError(DelightedBackoffError):
"""class representing 501 status code."""
pass


class DelightedBadGatewayError(DelightedBackoffError):
"""class representing 502 status code."""
pass


class DelightedServiceUnavailableError(DelightedBackoffError):
"""class representing 503 status code."""
pass
Expand Down Expand Up @@ -100,14 +90,6 @@ class DelightedServiceUnavailableError(DelightedBackoffError):
"raise_exception": DelightedInternalServerError,
"message": "The server encountered an unexpected condition which prevented it from fulfilling the request."
},
501: {
"raise_exception": DelightedNotImplementedError,
"message": "The server does not support the functionality required to fulfill the request."
},
502: {
"raise_exception": DelightedBadGatewayError,
"message": "Server received an invalid response."
},
503: {
"raise_exception": DelightedServiceUnavailableError,
"message": "API service is currently unavailable."
Expand Down
2 changes: 1 addition & 1 deletion tests/test_bookmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def calculate_new_bookmarks(self):
"survey_responses": {"updated_at": "2025-11-05T00:00:00.000000Z"},
"unsubscribes": {"unsubscribed_at": "2025-11-01T00:00:00.000000Z"},
"bounces": {"bounced_at": "2025-11-01T00:00:00.000000Z"},
"email_autopilot": {"updated_at": "2025-11-04T00:00:00.000000Z"},
"email_autopilot": {"updated_at": "2026-02-01T00:00:00.000000Z"},
}

return new_bookmarks
69 changes: 67 additions & 2 deletions tests/test_start_date.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,37 @@ class DelightedStartDateTest(StartDateTest, DelightedBaseTest):
"""Instantiate start date according to the desired data set and run the
test."""

# Per-class sync cache – isolates this class from other StartDateTest
# subclasses that also store results on the shared StartDateTest attributes.
_cached_record_count_1 = None
_cached_messages_1 = None
_cached_record_count_2 = None
_cached_messages_2 = None

def setUp(self):
# Restore or clear the shared StartDateTest cache with this class's
# own snapshot so setUp's condition works correctly.
StartDateTest.record_count_by_stream_1 = DelightedStartDateTest._cached_record_count_1
StartDateTest.synced_messages_by_stream_1 = DelightedStartDateTest._cached_messages_1
StartDateTest.record_count_by_stream_2 = DelightedStartDateTest._cached_record_count_2
StartDateTest.synced_messages_by_stream_2 = DelightedStartDateTest._cached_messages_2
super().setUp()

DelightedStartDateTest._cached_record_count_1 = StartDateTest.record_count_by_stream_1
DelightedStartDateTest._cached_messages_1 = StartDateTest.synced_messages_by_stream_1
DelightedStartDateTest._cached_record_count_2 = StartDateTest.record_count_by_stream_2
DelightedStartDateTest._cached_messages_2 = StartDateTest.synced_messages_by_stream_2

@staticmethod
def name():
return "tap_tester_delighted_start_date_test"

def streams_to_test(self):
streams_to_exclude = {"sms_autopilot", # We don't have API access to it
"metrics"} # FullTable stream
streams_to_exclude = {
"sms_autopilot", # We don't have API access to it
"metrics", # FullTable stream
"email_autopilot", # Tested separately with dates that produce differing record counts
}

return self.expected_stream_names().difference(streams_to_exclude)

Expand All @@ -23,3 +47,44 @@ def start_date_1(self):
@property
def start_date_2(self):
return "2025-11-05T00:00:00.000000Z"


class DelightedEmailAutopilotStartDateTest(StartDateTest, DelightedBaseTest):
"""Start date test specifically for email_autopilot.

The email_autopilot stream has records starting from 2025-12-09, so the
general test's start_date_2 (2025-11-05) yields the same 28 records as
start_date_1. Use dates where start_date_2 (2026-02-01) cuts the result
to 13 records while start_date_1 (2025-12-01) returns all 28.
"""
_cached_record_count_1 = None
_cached_messages_1 = None
_cached_record_count_2 = None
_cached_messages_2 = None

def setUp(self):
StartDateTest.record_count_by_stream_1 = DelightedEmailAutopilotStartDateTest._cached_record_count_1
StartDateTest.synced_messages_by_stream_1 = DelightedEmailAutopilotStartDateTest._cached_messages_1
StartDateTest.record_count_by_stream_2 = DelightedEmailAutopilotStartDateTest._cached_record_count_2
StartDateTest.synced_messages_by_stream_2 = DelightedEmailAutopilotStartDateTest._cached_messages_2
super().setUp()

DelightedEmailAutopilotStartDateTest._cached_record_count_1 = StartDateTest.record_count_by_stream_1
DelightedEmailAutopilotStartDateTest._cached_messages_1 = StartDateTest.synced_messages_by_stream_1
DelightedEmailAutopilotStartDateTest._cached_record_count_2 = StartDateTest.record_count_by_stream_2
DelightedEmailAutopilotStartDateTest._cached_messages_2 = StartDateTest.synced_messages_by_stream_2

@staticmethod
def name():
return "tap_tester_delighted_email_autopilot_start_date_test"

def streams_to_test(self):
return {"email_autopilot"}

@property
def start_date_1(self):
return "2025-12-01T00:00:00.000000Z"

@property
def start_date_2(self):
return "2026-02-01T00:00:00.000000Z"
53 changes: 50 additions & 3 deletions tests/unittests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,17 @@ def test_client_post(self, mock_make_request):
])
def test_make_request_http_failure_without_retry(self, test_name, error_code, mock_response, error, error_message):

with patch.object(self.client._session, "request", return_value=mock_response):
with patch.object(self.client._session, "request", return_value=mock_response) as mock_request:
with self.assertRaises(error) as e:
self.client._Client__make_request("GET", "https://api.example.com/resource")

expected_error_message = (f"HTTP-error-code: {error_code}, Error: {error_message}")
self.assertEqual(str(e.exception), expected_error_message)
self.assertEqual(mock_request.call_count, 1)

@parameterized.expand([
["429 error", 429, MockResponse(429), DelightedRateLimitError, "The API rate limit for your organisation/application pairing has been exceeded."],
["500 error", 500, MockResponse(500), DelightedInternalServerError, "The server encountered an unexpected condition which prevented it from fulfilling the request."],
["501 error", 501, MockResponse(501), DelightedNotImplementedError, "The server does not support the functionality required to fulfill the request."],
["502 error", 502, MockResponse(502), DelightedBadGatewayError, "Server received an invalid response."],
["503 error", 503, MockResponse(503), DelightedServiceUnavailableError, "API service is currently unavailable."],
])
@patch("time.sleep")
Expand Down Expand Up @@ -134,6 +133,54 @@ def test_make_request_other_failure_with_retry(self, test_name, error, mock_slee

self.assertEqual(mock_request.call_count, 5)

@parameterized.expand([
["501 error - Not Implemented", 501, "Unknown Error"],
["502 error - Bad Gateway", 502, "Unknown Error"],
["504 error - Gateway Timeout", 504, "Unknown Error"],
["505 error - HTTP Version Not Supported", 505, "Unknown Error"],
["506 error - Variant Also Negotiates", 506, "Unknown Error"],
["507 error - Insufficient Storage", 507, "Unknown Error"],
["508 error - Loop Detected", 508, "Unknown Error"],
["509 error - Bandwidth Limit Exceeded", 509, "Unknown Error"],
["510 error - Not Extended", 510, "Unknown Error"],
["511 error - Network Authentication Required", 511, "Unknown Error"],
])
@patch("time.sleep")
def test_unmapped_5xx_errors_trigger_backoff(self, test_name, error_code, error_message, mock_sleep):
"""Test that unmapped 5xx errors trigger backoff retry as DelightedBackoffError."""
mock_response = MockResponse(error_code)

with patch.object(self.client._session, "request", return_value=mock_response) as mock_request:
with self.assertRaises(DelightedBackoffError) as e:
self.client._Client__make_request("GET", "https://api.example.com/resource")

expected_error_message = f"HTTP-error-code: {error_code}, Error: {error_message}"
self.assertEqual(str(e.exception), expected_error_message)
# Verify backoff retry happened - should retry 5 times
self.assertEqual(mock_request.call_count, 5)

@parameterized.expand([
# Below 5xx range — DelightedError, no retry
["status_499", 499, DelightedError, 1],
# All 5xx errors (500-599) — DelightedBackoffError, retried
["status_500", 500, DelightedBackoffError, 5],
["status_550", 550, DelightedBackoffError, 5],
["status_599", 599, DelightedBackoffError, 5],
# Above 5xx range — DelightedError, no retry
["status_600", 600, DelightedError, 1],
])
@patch("time.sleep")
def test_5xx_range_boundary_checks(self, test_name, status_code, expected_exception, expected_call_count, mock_sleep):
"""Test that the correct exception is raised at 5xx range boundaries."""
mock_response = MockResponse(status_code)

with patch.object(self.client._session, "request", return_value=mock_response) as mock_request:
with self.assertRaises(expected_exception):
self.client._Client__make_request("GET", "https://api.example.com/resource")

# Verify retry behavior
self.assertEqual(mock_request.call_count, expected_call_count)

@patch("tap_delighted.client.Client.make_request")
def test_check_api_credentials(self, mock_make_request):
""" Test the API credentials check workflow """
Expand Down
Loading