From e9535ed46fb571ca22e1ab46a0bf7685be908dd8 Mon Sep 17 00:00:00 2001 From: "or.saiag" Date: Mon, 14 Sep 2026 16:09:26 +0300 Subject: [PATCH] fix(analysis): treat a deleted analysis (410 Gone) as a missing one The api service returns 410 Gone for an analysis that was deleted, expired or deprecated, so FileAnalysis/UrlAnalysis/EndpointAnalysis.from_analysis_id raised requests.HTTPError instead of returning None as documented, breaking cleanup paths that rely on the missing-analysis contract. Ref: TKT-18644 Co-Authored-By: Claude Opus 5 --- CHANGES | 4 ++++ intezer_sdk/__init__.py | 2 +- intezer_sdk/_api.py | 3 ++- intezer_sdk/base_analysis.py | 2 +- tests/unit/test_endpoint_analysis.py | 18 ++++++++++-------- tests/unit/test_file_analysis.py | 27 +++++++++++++++++++++++++++ tests/unit/test_url_analysis.py | 15 +++++++++++++++ 7 files changed, 60 insertions(+), 11 deletions(-) diff --git a/CHANGES b/CHANGES index 27650b5..d91f6b4 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,7 @@ +1.27.8 +------- +- Return None instead of raising when fetching a deleted analysis (410 Gone) by id, matching the documented from_analysis_id contract + 1.27.7 ------- - Include the server's error message in the HTTPError raised by raise_for_status for all error statuses diff --git a/intezer_sdk/__init__.py b/intezer_sdk/__init__.py index 9b39547..9630970 100644 --- a/intezer_sdk/__init__.py +++ b/intezer_sdk/__init__.py @@ -1 +1 @@ -__version__ = '1.27.7' +__version__ = '1.27.8' diff --git a/intezer_sdk/_api.py b/intezer_sdk/_api.py index d620e6d..310136e 100644 --- a/intezer_sdk/_api.py +++ b/intezer_sdk/_api.py @@ -911,7 +911,8 @@ def get_case_ttps(self, case_id: str) -> dict: @staticmethod def _assert_result_response(ignore_not_found: bool, response: Response): - statuses_to_ignore = [HTTPStatus.NOT_FOUND] if ignore_not_found else None + # A deleted, expired or deprecated analysis is returned as GONE, which callers treat as a missing analysis + statuses_to_ignore = [HTTPStatus.NOT_FOUND, HTTPStatus.GONE] if ignore_not_found else None raise_for_status(response, statuses_to_ignore=statuses_to_ignore) @staticmethod diff --git a/intezer_sdk/base_analysis.py b/intezer_sdk/base_analysis.py index da57d2e..a25cd22 100644 --- a/intezer_sdk/base_analysis.py +++ b/intezer_sdk/base_analysis.py @@ -152,7 +152,7 @@ def _assert_analysis_finished(self): @classmethod def _create_analysis_from_response(cls, response: Response, api: IntezerApiClient, analysis_id: str): - if response.status_code == HTTPStatus.NOT_FOUND: + if response.status_code in (HTTPStatus.NOT_FOUND, HTTPStatus.GONE): return None response_json = response.json() diff --git a/tests/unit/test_endpoint_analysis.py b/tests/unit/test_endpoint_analysis.py index fbe6a46..4cc6a95 100644 --- a/tests/unit/test_endpoint_analysis.py +++ b/tests/unit/test_endpoint_analysis.py @@ -93,18 +93,20 @@ def test_analysis_failed(self): with self.assertRaises(errors.AnalysisFailedError): EndpointAnalysis.from_analysis_id(analysis_id) - def test_analysis_not_found(self): - # Arrange + def test_analysis_missing_or_deleted(self): analysis_id = str(uuid.uuid4()) - with responses.RequestsMock() as mock: - mock.add('GET', url=f'{self.full_url}/endpoint-analyses/{analysis_id}', status=HTTPStatus.NOT_FOUND) + for status in (HTTPStatus.NOT_FOUND, HTTPStatus.GONE): + with self.subTest(status=status): + # Arrange + with responses.RequestsMock() as mock: + mock.add('GET', url=f'{self.full_url}/endpoint-analyses/{analysis_id}', status=status) - # Act - analysis = EndpointAnalysis.from_analysis_id(analysis_id) + # Act + analysis = EndpointAnalysis.from_analysis_id(analysis_id) - # Assert - self.assertIsNone(analysis) + # Assert + self.assertIsNone(analysis) def test_get_sub_analyses(self): # Arrange diff --git a/tests/unit/test_file_analysis.py b/tests/unit/test_file_analysis.py index b2ad2d8..f137818 100644 --- a/tests/unit/test_file_analysis.py +++ b/tests/unit/test_file_analysis.py @@ -13,6 +13,7 @@ from intezer_sdk import consts from intezer_sdk import errors +from intezer_sdk._api import IntezerApi from intezer_sdk.analysis import FileAnalysis from intezer_sdk.api import get_global_api from intezer_sdk.consts import OnPremiseVersion @@ -1101,6 +1102,32 @@ def test_get_analysis_by_id_in_progress(self): self.assertEqual(consts.AnalysisStatusCode.IN_PROGRESS, analysis.status) self.assertEqual(analysis_id, analysis.analysis_id) + def test_get_analysis_by_id_returns_none_when_analysis_is_missing_or_deleted(self): + analysis_id = 'analysis_id' + + for status in (HTTPStatus.NOT_FOUND, HTTPStatus.GONE): + with self.subTest(status=status): + # Arrange + with responses.RequestsMock() as mock: + mock.add('GET', url=f'{self.full_url}/analyses/{analysis_id}', status=status) + + # Act + analysis = FileAnalysis.from_analysis_id(analysis_id) + + # Assert + self.assertIsNone(analysis) + + def test_get_file_analysis_response_raises_when_analysis_is_deleted_and_not_found_is_not_ignored(self): + # Arrange + analysis_id = 'analysis_id' + + with responses.RequestsMock() as mock: + mock.add('GET', url=f'{self.full_url}/analyses/{analysis_id}', status=HTTPStatus.GONE) + + # Act & Assert + with self.assertRaises(requests.HTTPError): + IntezerApi(get_global_api()).get_file_analysis_response(analysis_id, False) + def test_download_file_path_uses_content_disposition(self): # Arrange file_hash = 'hash' diff --git a/tests/unit/test_url_analysis.py b/tests/unit/test_url_analysis.py index 539fb89..c9049cc 100644 --- a/tests/unit/test_url_analysis.py +++ b/tests/unit/test_url_analysis.py @@ -64,6 +64,21 @@ def test_get_analysis_by_id_raises_when_analysis_failed(self): with self.assertRaises(errors.AnalysisFailedError): UrlAnalysis.from_analysis_id(analysis_id) + def test_get_analysis_by_id_returns_none_when_analysis_is_missing_or_deleted(self): + analysis_id = 'analysis_id' + + for status in (HTTPStatus.NOT_FOUND, HTTPStatus.GONE): + with self.subTest(status=status): + # Arrange + with responses.RequestsMock() as mock: + mock.add('GET', url=f'{self.full_url}/url/{analysis_id}', status=status) + + # Act + analysis = UrlAnalysis.from_analysis_id(analysis_id) + + # Assert + self.assertIsNone(analysis) + def test_send_perform_request_and_sets_analysis_status(self): # Arrange analysis_id = str(uuid.uuid4())