From 2a4ba5d893f1f464078c7d10a9098b5172206c44 Mon Sep 17 00:00:00 2001 From: Vi6hal <20889199+Vi6hal@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:40:14 +0000 Subject: [PATCH 01/12] remove action_destination & action_target_id --- tap_facebook/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tap_facebook/__init__.py b/tap_facebook/__init__.py index 8935194..cfcb5b4 100755 --- a/tap_facebook/__init__.py +++ b/tap_facebook/__init__.py @@ -600,8 +600,6 @@ def sync(self): ALL_ACTION_BREAKDOWNS = [ 'action_type', - 'action_target_id', - 'action_destination' ] def get_start(stream, bookmark_key): From 41d8312a8c3788d87e51f935ab6625eacc3631b2 Mon Sep 17 00:00:00 2001 From: Vi6hal <20889199+Vi6hal@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:40:31 +0000 Subject: [PATCH 02/12] change to use summary_action_breakdowns --- tap_facebook/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tap_facebook/__init__.py b/tap_facebook/__init__.py index cfcb5b4..c3e2f4e 100755 --- a/tap_facebook/__init__.py +++ b/tap_facebook/__init__.py @@ -598,9 +598,7 @@ def sync(self): '28d_view' ] -ALL_ACTION_BREAKDOWNS = [ - 'action_type', -] +ALL_ACTION_BREAKDOWNS = ['action_type','action_target_id','action_destination'] def get_start(stream, bookmark_key): tap_stream_id = stream.name @@ -701,7 +699,7 @@ def job_params(self): while buffered_start_date <= end_date: yield { 'level': self.level, - 'action_breakdowns': list(self.action_breakdowns), + 'summary_action_breakdowns': list(self.action_breakdowns), 'breakdowns': list(self.breakdowns), 'limit': self.limit, 'fields': list(self.fields().difference(self.invalid_insights_fields)), From 489fc49be2ed95f074e9dbcf7ede56496ee90a73 Mon Sep 17 00:00:00 2001 From: Vi6hal <20889199+Vi6hal@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:00:42 +0000 Subject: [PATCH 03/12] add err_handling to action_breakdown --- tap_facebook/__init__.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tap_facebook/__init__.py b/tap_facebook/__init__.py index c3e2f4e..3cd2e9b 100755 --- a/tap_facebook/__init__.py +++ b/tap_facebook/__init__.py @@ -809,15 +809,31 @@ def __iter__(self): "primary-keys": ['hourly_stats_aggregated_by_advertiser_time_zone']}, } +def parse_action_breakdowns(breakdown_str): + valid_breakdowns = [] + if breakdown_str: + act_breakdowns = [b.strip() for b in str(breakdown_str).split(',')] + for breakdown in act_breakdowns: + if not breakdown: # Skip empty strings + continue + if breakdown in ALL_ACTION_BREAKDOWNS: + valid_breakdowns.append(breakdown) + else: + LOGGER.warning("Invalid action breakdown %s", breakdown) + return valid_breakdowns if valid_breakdowns else ALL_ACTION_BREAKDOWNS def initialize_stream(account, catalog_entry, state): # pylint: disable=too-many-return-statements name = catalog_entry.stream stream_alias = catalog_entry.stream_alias + if not CONFIG.get("action_breakdowns", None): + CONFIG["action_breakdowns"] = "action_type" + valid_breakdowns = parse_action_breakdowns(CONFIG.get("action_breakdowns", None)) + LOGGER.info("Using Breakdowns %s", valid_breakdowns) if name in INSIGHTS_BREAKDOWNS_OPTIONS: return AdsInsights(name, account, stream_alias, catalog_entry, state=state, - options=INSIGHTS_BREAKDOWNS_OPTIONS[name]) + options=INSIGHTS_BREAKDOWNS_OPTIONS[name], action_breakdowns=valid_breakdowns) elif name == 'campaigns': return Campaigns(name, account, stream_alias, catalog_entry, state=state) elif name == 'adsets': From e6fadfbc92e5d2376240babee2ea3fc4351616e9 Mon Sep 17 00:00:00 2001 From: Vi6hal <20889199+Vi6hal@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:10:14 +0000 Subject: [PATCH 04/12] remove summary effect --- tap_facebook/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tap_facebook/__init__.py b/tap_facebook/__init__.py index 3cd2e9b..74efee9 100755 --- a/tap_facebook/__init__.py +++ b/tap_facebook/__init__.py @@ -699,7 +699,7 @@ def job_params(self): while buffered_start_date <= end_date: yield { 'level': self.level, - 'summary_action_breakdowns': list(self.action_breakdowns), + 'action_breakdowns': list(self.action_breakdowns), 'breakdowns': list(self.breakdowns), 'limit': self.limit, 'fields': list(self.fields().difference(self.invalid_insights_fields)), From 3c3e477c5fedc3554fcffa5bd3bd3bd005a9c4d2 Mon Sep 17 00:00:00 2001 From: Vi6hal <20889199+Vi6hal@users.noreply.github.com> Date: Thu, 27 Nov 2025 05:15:43 +0000 Subject: [PATCH 05/12] move impl to init --- tap_facebook/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tap_facebook/__init__.py b/tap_facebook/__init__.py index 74efee9..74fe092 100755 --- a/tap_facebook/__init__.py +++ b/tap_facebook/__init__.py @@ -812,7 +812,7 @@ def __iter__(self): def parse_action_breakdowns(breakdown_str): valid_breakdowns = [] if breakdown_str: - act_breakdowns = [b.strip() for b in str(breakdown_str).split(',')] + act_breakdowns = [b.strip().lower() for b in str(breakdown_str).split(',')] for breakdown in act_breakdowns: if not breakdown: # Skip empty strings continue @@ -826,14 +826,10 @@ def initialize_stream(account, catalog_entry, state): # pylint: disable=too-many name = catalog_entry.stream stream_alias = catalog_entry.stream_alias - if not CONFIG.get("action_breakdowns", None): - CONFIG["action_breakdowns"] = "action_type" - valid_breakdowns = parse_action_breakdowns(CONFIG.get("action_breakdowns", None)) - LOGGER.info("Using Breakdowns %s", valid_breakdowns) if name in INSIGHTS_BREAKDOWNS_OPTIONS: return AdsInsights(name, account, stream_alias, catalog_entry, state=state, - options=INSIGHTS_BREAKDOWNS_OPTIONS[name], action_breakdowns=valid_breakdowns) + options=INSIGHTS_BREAKDOWNS_OPTIONS[name], action_breakdowns=CONFIG["action_breakdowns"]) elif name == 'campaigns': return Campaigns(name, account, stream_alias, catalog_entry, state=state) elif name == 'adsets': @@ -971,6 +967,12 @@ def main_impl(): request_timeout = float(config_request_timeout) else: request_timeout = REQUEST_TIMEOUT # If value is 0,"0","" or not passed then set default to 300 seconds. + + ab_params = CONFIG.get("action_breakdowns") + + CONFIG["action_breakdowns"] = ALL_ACTION_BREAKDOWNS if not ab_params else parse_action_breakdowns(ab_params) + + LOGGER.info("Using Breakdowns %s", CONFIG["action_breakdowns"]) global API API = FacebookAdsApi.init(access_token=access_token, timeout=request_timeout) From 982dd63d2da4416cea5ef32756461e8de6ed6e2a Mon Sep 17 00:00:00 2001 From: Vi6hal <20889199+Vi6hal@users.noreply.github.com> Date: Tue, 9 Dec 2025 06:12:40 +0000 Subject: [PATCH 06/12] minor fixes --- tap_facebook/__init__.py | 35 +++++++++++++++++++------------ tests/test_facebook_all_fields.py | 1 + 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/tap_facebook/__init__.py b/tap_facebook/__init__.py index 74fe092..7762dc9 100755 --- a/tap_facebook/__init__.py +++ b/tap_facebook/__init__.py @@ -598,7 +598,11 @@ def sync(self): '28d_view' ] -ALL_ACTION_BREAKDOWNS = ['action_type','action_target_id','action_destination'] +ALL_ACTION_BREAKDOWNS = [ + 'action_type', + 'action_target_id', + 'action_destination' +] def get_start(stream, bookmark_key): tap_stream_id = stream.name @@ -810,16 +814,21 @@ def __iter__(self): } def parse_action_breakdowns(breakdown_str): + if not breakdown_str: + return ALL_ACTION_BREAKDOWNS + if not isinstance(breakdown_str, str): + LOGGER.warning("action_breakdowns must be a string, got %s", type(breakdown_str)) + return ALL_ACTION_BREAKDOWNS + valid_breakdowns = [] - if breakdown_str: - act_breakdowns = [b.strip().lower() for b in str(breakdown_str).split(',')] - for breakdown in act_breakdowns: - if not breakdown: # Skip empty strings - continue - if breakdown in ALL_ACTION_BREAKDOWNS: - valid_breakdowns.append(breakdown) - else: - LOGGER.warning("Invalid action breakdown %s", breakdown) + act_breakdowns = [b.strip().lower() for b in breakdown_str.split(',')] + for breakdown in act_breakdowns: + if not breakdown: # Skip empty strings + continue + if breakdown in ALL_ACTION_BREAKDOWNS: + valid_breakdowns.append(breakdown) + else: + LOGGER.warning("Invalid action breakdown %s", breakdown) return valid_breakdowns if valid_breakdowns else ALL_ACTION_BREAKDOWNS def initialize_stream(account, catalog_entry, state): # pylint: disable=too-many-return-statements @@ -969,10 +978,10 @@ def main_impl(): request_timeout = REQUEST_TIMEOUT # If value is 0,"0","" or not passed then set default to 300 seconds. ab_params = CONFIG.get("action_breakdowns") + parsed_breakdowns = parse_action_breakdowns(ab_params) if ab_params else ALL_ACTION_BREAKDOWNS + CONFIG["action_breakdowns"] = parsed_breakdowns - CONFIG["action_breakdowns"] = ALL_ACTION_BREAKDOWNS if not ab_params else parse_action_breakdowns(ab_params) - - LOGGER.info("Using Breakdowns %s", CONFIG["action_breakdowns"]) + LOGGER.info("Using %d action breakdown(s): %s", len(parsed_breakdowns), parsed_breakdowns) global API API = FacebookAdsApi.init(access_token=access_token, timeout=request_timeout) diff --git a/tests/test_facebook_all_fields.py b/tests/test_facebook_all_fields.py index 94f01b1..993aa39 100644 --- a/tests/test_facebook_all_fields.py +++ b/tests/test_facebook_all_fields.py @@ -41,6 +41,7 @@ class FacebookAllFieldsTest(AllFieldsTest, FacebookBaseTest): 'image_crops', 'product_set_id', 'url_tags', + "video_id", 'applink_treatment', 'object_id', 'link_og_id', From 7c4e89edd4fbdf72ddc7beeee1039614ca96b36d Mon Sep 17 00:00:00 2001 From: Vi6hal <20889199+Vi6hal@users.noreply.github.com> Date: Tue, 9 Dec 2025 06:31:19 +0000 Subject: [PATCH 07/12] remove video_id --- tests/test_facebook_all_fields.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_facebook_all_fields.py b/tests/test_facebook_all_fields.py index 7736317..b6cea16 100644 --- a/tests/test_facebook_all_fields.py +++ b/tests/test_facebook_all_fields.py @@ -41,7 +41,6 @@ class FacebookAllFieldsTest(AllFieldsTest, FacebookBaseTest): 'image_crops', 'product_set_id', 'url_tags', - "video_id", 'applink_treatment', 'object_id', 'link_og_id', @@ -51,7 +50,6 @@ class FacebookAllFieldsTest(AllFieldsTest, FacebookBaseTest): 'link_url', 'adlabels', 'source_instagram_media_id', - 'video_id' }, "ads_insights_country": { 'video_p75_watched_actions', From 1c118fe73a0617c77375eba4196c6331d8b58e8f Mon Sep 17 00:00:00 2001 From: Vi6hal <20889199+Vi6hal@users.noreply.github.com> Date: Tue, 9 Dec 2025 06:36:46 +0000 Subject: [PATCH 08/12] add new test --- tap_facebook/__init__.py | 8 +- tests/unittests/test_action_breakdowns.py | 126 ++++++++++++++++++++++ 2 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 tests/unittests/test_action_breakdowns.py diff --git a/tap_facebook/__init__.py b/tap_facebook/__init__.py index 7762dc9..ce1b50f 100755 --- a/tap_facebook/__init__.py +++ b/tap_facebook/__init__.py @@ -820,16 +820,16 @@ def parse_action_breakdowns(breakdown_str): LOGGER.warning("action_breakdowns must be a string, got %s", type(breakdown_str)) return ALL_ACTION_BREAKDOWNS - valid_breakdowns = [] + selected_breakdowns = [] act_breakdowns = [b.strip().lower() for b in breakdown_str.split(',')] for breakdown in act_breakdowns: if not breakdown: # Skip empty strings continue - if breakdown in ALL_ACTION_BREAKDOWNS: - valid_breakdowns.append(breakdown) + if breakdown in ALL_ACTION_BREAKDOWNS and breakdown not in selected_breakdowns: + selected_breakdowns.append(breakdown) else: LOGGER.warning("Invalid action breakdown %s", breakdown) - return valid_breakdowns if valid_breakdowns else ALL_ACTION_BREAKDOWNS + return selected_breakdowns if selected_breakdowns else ALL_ACTION_BREAKDOWNS def initialize_stream(account, catalog_entry, state): # pylint: disable=too-many-return-statements diff --git a/tests/unittests/test_action_breakdowns.py b/tests/unittests/test_action_breakdowns.py new file mode 100644 index 0000000..dce82b4 --- /dev/null +++ b/tests/unittests/test_action_breakdowns.py @@ -0,0 +1,126 @@ +import unittest +from tap_facebook import parse_action_breakdowns, ALL_ACTION_BREAKDOWNS + + +class TestParseActionBreakdowns(unittest.TestCase): + """Test suite for parse_action_breakdowns function""" + + def test_none_returns_all_breakdowns(self): + """Test that None input returns all default breakdowns""" + result = parse_action_breakdowns(None) + self.assertEqual(result, ALL_ACTION_BREAKDOWNS) + self.assertEqual(len(result), 3) + + def test_empty_string_returns_all_breakdowns(self): + """Test that empty string returns all default breakdowns""" + result = parse_action_breakdowns("") + self.assertEqual(result, ALL_ACTION_BREAKDOWNS) + self.assertEqual(len(result), 3) + + def test_non_string_type_returns_all_breakdowns(self): + """Test that non-string types return all default breakdowns with warning""" + result = parse_action_breakdowns(123) + self.assertEqual(result, ALL_ACTION_BREAKDOWNS) + + result = parse_action_breakdowns(['action_type']) + self.assertEqual(result, ALL_ACTION_BREAKDOWNS) + + result = parse_action_breakdowns({'breakdown': 'action_type'}) + self.assertEqual(result, ALL_ACTION_BREAKDOWNS) + + def test_single_valid_breakdown(self): + """Test parsing a single valid breakdown""" + result = parse_action_breakdowns("action_type") + self.assertEqual(result, ['action_type']) + self.assertEqual(len(result), 1) + + def test_multiple_valid_breakdowns(self): + """Test parsing multiple valid breakdowns""" + result = parse_action_breakdowns("action_type,action_destination") + self.assertEqual(result, ['action_type', 'action_destination']) + self.assertEqual(len(result), 2) + + def test_all_valid_breakdowns(self): + """Test parsing all three valid breakdowns""" + result = parse_action_breakdowns("action_type,action_target_id,action_destination") + self.assertEqual(result, ['action_type', 'action_target_id', 'action_destination']) + self.assertEqual(len(result), 3) + + def test_whitespace_handling(self): + """Test that whitespace is properly stripped""" + result = parse_action_breakdowns(" action_type , action_destination ") + self.assertEqual(result, ['action_type', 'action_destination']) + + result = parse_action_breakdowns(" action_type , action_target_id ") + self.assertEqual(result, ['action_type', 'action_target_id']) + + def test_case_insensitivity(self): + """Test that input is case-insensitive""" + result = parse_action_breakdowns("ACTION_TYPE,Action_Destination") + self.assertEqual(result, ['action_type', 'action_destination']) + + result = parse_action_breakdowns("ACTION_TARGET_ID") + self.assertEqual(result, ['action_target_id']) + + def test_mixed_valid_and_invalid(self): + """Test parsing mix of valid and invalid breakdowns""" + result = parse_action_breakdowns("action_type,invalid_breakdown,action_destination") + self.assertEqual(result, ['action_type', 'action_destination']) + self.assertEqual(len(result), 2) + + result = parse_action_breakdowns("invalid1,action_type,invalid2") + self.assertEqual(result, ['action_type']) + + def test_all_invalid_returns_all_breakdowns(self): + """Test that all invalid values returns default breakdowns""" + result = parse_action_breakdowns("invalid1,invalid2,invalid3") + self.assertEqual(result, ALL_ACTION_BREAKDOWNS) + + result = parse_action_breakdowns("foo,bar,baz") + self.assertEqual(result, ALL_ACTION_BREAKDOWNS) + + def test_empty_values_skipped(self): + """Test that empty values from comma-separation are skipped""" + result = parse_action_breakdowns("action_type,,action_destination") + self.assertEqual(result, ['action_type', 'action_destination']) + + result = parse_action_breakdowns(",action_type,") + self.assertEqual(result, ['action_type']) + + result = parse_action_breakdowns(",,action_type,,") + self.assertEqual(result, ['action_type']) + + def test_only_commas_returns_all_breakdowns(self): + """Test that only commas returns default breakdowns""" + result = parse_action_breakdowns(",,,") + self.assertEqual(result, ALL_ACTION_BREAKDOWNS) + + def test_duplicate_breakdowns(self): + """Test handling of duplicate breakdown values""" + result = parse_action_breakdowns("action_type,action_type,action_destination") + # Should not include duplicates since deduplication is performed + self.assertEqual(result, ['action_type', 'action_destination']) + + def test_whitespace_only_string(self): + """Test whitespace-only string is treated as empty""" + result = parse_action_breakdowns(" ") + self.assertEqual(result, ALL_ACTION_BREAKDOWNS) + + result = parse_action_breakdowns("\t\n") + self.assertEqual(result, ALL_ACTION_BREAKDOWNS) + + def test_partial_match_not_accepted(self): + """Test that partial matches are not accepted""" + result = parse_action_breakdowns("action_typ") + self.assertEqual(result, ALL_ACTION_BREAKDOWNS) + + result = parse_action_breakdowns("action_type_extra") + self.assertEqual(result, ALL_ACTION_BREAKDOWNS) + + def test_order_preserved(self): + """Test that order of valid breakdowns is preserved""" + result = parse_action_breakdowns("action_destination,action_type") + self.assertEqual(result, ['action_destination', 'action_type']) + + result = parse_action_breakdowns("action_target_id,action_destination,action_type") + self.assertEqual(result, ['action_target_id', 'action_destination', 'action_type']) \ No newline at end of file From 2ca7fcf2a39d8a54eacb21be0654d8edff16822c Mon Sep 17 00:00:00 2001 From: Vi6hal <20889199+Vi6hal@users.noreply.github.com> Date: Tue, 9 Dec 2025 07:06:30 +0000 Subject: [PATCH 09/12] changelog and setup --- CHANGELOG.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a01796..2c9f1b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 1.24.0 + * Make action_breakdowns configurable [#260](https://github.com/singer-io/tap-facebook/pull/260) + ## 1.24.0 * Bump facebook_business SDK to v23.0.1 [#255](https://github.com/singer-io/tap-facebook/pull/255) * Remove Deprecated Fields from adcreative [#255](https://github.com/singer-io/tap-facebook/pull/255) diff --git a/setup.py b/setup.py index df0a97b..8261ae3 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup setup(name='tap-facebook', - version='1.24.0', + version='1.25.0', description='Singer.io tap for extracting data from the Facebook Ads API', author='Stitch', url='https://singer.io', From 58fd486d08c43dcee06c0aacd0ce8fc0a676ae08 Mon Sep 17 00:00:00 2001 From: Vi6hal <20889199+Vi6hal@users.noreply.github.com> Date: Tue, 9 Dec 2025 07:19:41 +0000 Subject: [PATCH 10/12] copilot suggestions --- CHANGELOG.md | 2 +- tap_facebook/__init__.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c9f1b0..b04ac96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 1.24.0 +## 1.25.0 * Make action_breakdowns configurable [#260](https://github.com/singer-io/tap-facebook/pull/260) ## 1.24.0 diff --git a/tap_facebook/__init__.py b/tap_facebook/__init__.py index ce1b50f..98847e5 100755 --- a/tap_facebook/__init__.py +++ b/tap_facebook/__init__.py @@ -826,7 +826,7 @@ def parse_action_breakdowns(breakdown_str): if not breakdown: # Skip empty strings continue if breakdown in ALL_ACTION_BREAKDOWNS and breakdown not in selected_breakdowns: - selected_breakdowns.append(breakdown) + selected_breakdowns.append(breakdown) else: LOGGER.warning("Invalid action breakdown %s", breakdown) return selected_breakdowns if selected_breakdowns else ALL_ACTION_BREAKDOWNS @@ -978,10 +978,9 @@ def main_impl(): request_timeout = REQUEST_TIMEOUT # If value is 0,"0","" or not passed then set default to 300 seconds. ab_params = CONFIG.get("action_breakdowns") - parsed_breakdowns = parse_action_breakdowns(ab_params) if ab_params else ALL_ACTION_BREAKDOWNS - CONFIG["action_breakdowns"] = parsed_breakdowns + CONFIG["action_breakdowns"] = parse_action_breakdowns(ab_params) - LOGGER.info("Using %d action breakdown(s): %s", len(parsed_breakdowns), parsed_breakdowns) + LOGGER.info("Using %d action breakdown(s): %s", len( CONFIG["action_breakdowns"]), CONFIG["action_breakdowns"]) global API API = FacebookAdsApi.init(access_token=access_token, timeout=request_timeout) From ee0c3e1c66a656145cb1009b6253ed9029533674 Mon Sep 17 00:00:00 2001 From: Vishal Pachpinde Date: Tue, 9 Dec 2025 12:57:02 +0530 Subject: [PATCH 11/12] Update tap_facebook/__init__.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tap_facebook/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tap_facebook/__init__.py b/tap_facebook/__init__.py index 98847e5..b76b410 100755 --- a/tap_facebook/__init__.py +++ b/tap_facebook/__init__.py @@ -980,7 +980,7 @@ def main_impl(): ab_params = CONFIG.get("action_breakdowns") CONFIG["action_breakdowns"] = parse_action_breakdowns(ab_params) - LOGGER.info("Using %d action breakdown(s): %s", len( CONFIG["action_breakdowns"]), CONFIG["action_breakdowns"]) + LOGGER.info("Using %d action breakdown(s): %s", len(CONFIG["action_breakdowns"]), CONFIG["action_breakdowns"]) global API API = FacebookAdsApi.init(access_token=access_token, timeout=request_timeout) From 16af03c186942c295c3836652ff312bef5ca4a99 Mon Sep 17 00:00:00 2001 From: Vishal Pachpinde Date: Thu, 18 Dec 2025 16:37:11 +0530 Subject: [PATCH 12/12] Fix missing newline at end of test file --- tests/unittests/test_action_breakdowns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unittests/test_action_breakdowns.py b/tests/unittests/test_action_breakdowns.py index dce82b4..045bdf1 100644 --- a/tests/unittests/test_action_breakdowns.py +++ b/tests/unittests/test_action_breakdowns.py @@ -123,4 +123,4 @@ def test_order_preserved(self): self.assertEqual(result, ['action_destination', 'action_type']) result = parse_action_breakdowns("action_target_id,action_destination,action_type") - self.assertEqual(result, ['action_target_id', 'action_destination', 'action_type']) \ No newline at end of file + self.assertEqual(result, ['action_target_id', 'action_destination', 'action_type'])