From 828df68cd6f3340ccaaa64788a17a23919a768e3 Mon Sep 17 00:00:00 2001 From: Thomas Graf Date: Sat, 26 Oct 2024 09:33:20 +0200 Subject: [PATCH 1/6] WIP: feat: implement recent_xxx endpoints --- ChangeLog.md | 8 +++++++- sw360/components.py | 15 ++++++++++++++- sw360/releases.py | 13 +++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 83b0f08..f9276d9 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,12 @@ # SW360 Base Library for Python +## NEXT + +* more REST API endpoints implemented: + * `get_recent_releases` + * `get_recent_components` + ## V1.6.0 * packages REST API calls implemented. @@ -21,7 +27,7 @@ * when using CaPyCLI in a CI pipeline, connection problems to the SW360 server (5xx) cause the pipeline to fail. We have now add an improved session handling to all api requests. -* dependency updates due to security vulnerabilities in idna. +* dependency updates due to security vulnerabilities in `idna`. ## V1.4.1 diff --git a/sw360/components.py b/sw360/components.py index bb317f7..94b92a9 100644 --- a/sw360/components.py +++ b/sw360/components.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2019-2023 Siemens +# Copyright (c) 2019-2024 Siemens # Copyright (c) 2022 BMW CarIT GmbH # All Rights Reserved. # Authors: thomas.graf@siemens.com, gernot.hillier@siemens.com @@ -292,3 +292,16 @@ def get_users_of_component(self, component_id: str) -> Optional[Dict[str, Any]]: resp = self.api_get(self.url + "resource/api/components/usedBy/" + component_id) return resp + + def get_recent_components(self) -> Optional[Dict[str, Any]]: + """Get 5 of the service's most recently created components. + + API endpoint: GET /components/recentComponents + + :return: a list of components + :rtype: JSON list of component objects + :raises SW360Error: if there is a negative HTTP response + """ + url = self.url + "resource/api/components/recentComponents" + resp = self.api_get(url) + return resp diff --git a/sw360/releases.py b/sw360/releases.py index ca2cf3c..4a1dcd1 100644 --- a/sw360/releases.py +++ b/sw360/releases.py @@ -286,3 +286,16 @@ def unlink_packages_from_release(self, release_id: str, packages: List[str]) -> url = self.url + "resource/api/releases/" + release_id + "/unlink/packages/" return self.api_patch(url, json=packages) + + def get_recent_releases(self) -> Optional[Dict[str, Any]]: + """Get 5 of the service's most recently created releases. + + API endpoint: GET /releases/recentReleases + + :return: a list of releases + :rtype: JSON list of release objects + :raises SW360Error: if there is a negative HTTP response + """ + url = self.url + "resource/api/releases/recentReleases" + resp = self.api_get(url) + return resp From 6e788a59d3ecb10678b9612e7cb4d83a794bee97 Mon Sep 17 00:00:00 2001 From: Thomas Graf Date: Sat, 26 Oct 2024 18:49:16 +0200 Subject: [PATCH 2/6] feat: add `get_recent_components` --- ChangeLog.md | 3 ++ sw360/components.py | 15 ++++---- tests/test_sw360_components.py | 66 ++++++++++++++++++++++++++++++++-- 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index f9276d9..dacd7e9 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -10,6 +10,9 @@ * more REST API endpoints implemented: * `get_recent_releases` * `get_recent_components` + * `get_all_moderation_requests` + * `get_moderation_requests_by_state` + * `get_moderation_request` ## V1.6.0 diff --git a/sw360/components.py b/sw360/components.py index 94b92a9..9683d8d 100644 --- a/sw360/components.py +++ b/sw360/components.py @@ -84,16 +84,10 @@ def get_components_by_type(self, component_type: str) -> List[Dict[str, Any]]: """ resp = self.api_get(self.url + "resource/api/components?type=" + component_type) - if not resp: - return [] - - if "_embedded" not in resp: - return [] - - if "sw360:components" not in resp["_embedded"]: - return [] + if resp and ("_embedded" in resp) and ("sw360:components" in resp["_embedded"]): + return resp["_embedded"]["sw360:components"] - return resp["_embedded"]["sw360:components"] + return [] def get_component(self, component_id: str) -> Optional[Dict[str, Any]]: """Get information of about a component @@ -304,4 +298,7 @@ def get_recent_components(self) -> Optional[Dict[str, Any]]: """ url = self.url + "resource/api/components/recentComponents" resp = self.api_get(url) + if resp and ("_embedded" in resp) and ("sw360:components" in resp["_embedded"]): + return resp["_embedded"]["sw360:components"] + return resp diff --git a/tests/test_sw360_components.py b/tests/test_sw360_components.py index 0693ab8..6db3201 100644 --- a/tests/test_sw360_components.py +++ b/tests/test_sw360_components.py @@ -839,11 +839,73 @@ def xtest_get_component_real(self) -> None: lib.login_api() # c = lib.get_component("eaba2f0416e000e8ca5b2ccb4400633e") # c = lib.get_components_by_external_id( "package-url", "pkg:nuget/Tethys.Logging") - c = lib.api_get("https://sw360.siemens.com/resource/api/components/searchByExternalIds?package-url=pkg:nuget/Tethys.Logging") # noqa + c = lib.api_get("https://my.server.com/resource/api/components/searchByExternalIds?package-url=pkg:nuget/Tethys.Logging") # noqa print(c) + @responses.activate + def test_get_recent_components(self) -> None: + lib = SW360(self.MYURL, self.MYTOKEN, False) + lib.force_no_session = True + self._add_login_response() + actual = lib.login_api() + self.assertTrue(actual) + + responses.add( + method=responses.GET, + url=self.MYURL + "resource/api/components/recentComponents", + body='''{ + "_embedded": { + "sw360:components": [ + { + "id": "ff6f1b5b212b4f93b306e2cceca4f64d", + "name": "intl-listformat", + "description": "n/a", + "componentType": "OSS", + "visbility": "EVERYONE", + "mainLicenseIds": [], + "_links": { + "self": { + "href": "https://my.server.com/resource/api/components/ff" + } + } + }, + { + "id": "f916b35d6c864014bd8823c45615aeab", + "name": "fields-metadata-plugin", + "description": "n/a", + "componentType": "OSS", + "visbility": "EVERYONE", + "mainLicenseIds": [], + "_links": { + "self": { + "href": "https://my.server.com/resource/api/components/f9" + } + } + } + ] + }, + "_links": { + "curies": [ + { + "href": "https://my.server.com/resource/docs/{rel}.html", + "name": "sw360", + "templated": true + } + ] + } + }''', + status=200, + content_type="application/json", + adding_headers={"Authorization": "Token " + self.MYTOKEN}, + ) + + components = lib.get_recent_components() + self.assertIsNotNone(components) + self.assertEqual(2, len(components)) + self.assertEqual("intl-listformat", components[0]["name"]) + self.assertEqual("OSS", components[0]["componentType"]) + if __name__ == "__main__": - # unittest.main() x = Sw360TestComponents() x.test_get_all_components_with_fields_and_paging() From 7daf274f87dc49c0b19e1c89795ade07e10680e7 Mon Sep 17 00:00:00 2001 From: Thomas Graf Date: Sat, 26 Oct 2024 18:49:35 +0200 Subject: [PATCH 3/6] feat: add `get_recent_releases` --- sw360/releases.py | 3 ++ tests/test_sw360_releases.py | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/sw360/releases.py b/sw360/releases.py index 4a1dcd1..a6852dc 100644 --- a/sw360/releases.py +++ b/sw360/releases.py @@ -298,4 +298,7 @@ def get_recent_releases(self) -> Optional[Dict[str, Any]]: """ url = self.url + "resource/api/releases/recentReleases" resp = self.api_get(url) + if resp and ("_embedded" in resp) and ("sw360:releases" in resp["_embedded"]): + return resp["_embedded"]["sw360:releases"] + return resp diff --git a/tests/test_sw360_releases.py b/tests/test_sw360_releases.py index 44cc69e..5a99f10 100644 --- a/tests/test_sw360_releases.py +++ b/tests/test_sw360_releases.py @@ -616,6 +616,64 @@ def test_unlink_packages_from_release_no_id(self) -> None: self.assertEqual("No release id provided!", context.exception.message) + @responses.activate + def test_get_recent_releases(self) -> None: + lib = SW360(self.MYURL, self.MYTOKEN, False) + self._add_login_response() + actual = lib.login_api() + self.assertTrue(actual) + + responses.add( + method=responses.GET, + url=self.MYURL + "resource/api/releases/recentReleases", + body=''' + { + "_embedded": { + "sw360:releases": [ + { + "id": "f23200c333564eb98bbd5823937d5fc8", + "name": "MarkupSafe", + "version": "3.0.2", + "_links": { + "self": { + "href": "https://my.server.com/resource/api/releases/f2" + } + } + }, + { + "id": "d39333c659d64ee3aa30d48cc0bcd930", + "name": "HTTPCore", + "version": "1.0.6", + "_links": { + "self": { + "href": "https://my.server.com/resource/api/releases/d3" + } + } + } + ] + }, + "_links": { + "curies": [ + { + "href": "https://my.server.com/resource/docs/{rel}.html", + "name": "sw360", + "templated": true + } + ] + } + } + ''', + status=200, + content_type="application/json", + adding_headers={"Authorization": "Token " + self.MYTOKEN}, + ) + + releases = lib.get_recent_releases() + self.assertIsNotNone(releases) + self.assertEqual(2, len(releases)) + self.assertEqual("MarkupSafe", releases[0]["name"]) + self.assertEqual("3.0.2", releases[0]["version"]) + if __name__ == "__main__": unittest.main() From 048db59199311eadb423fe428e343b3ea379534a Mon Sep 17 00:00:00 2001 From: Thomas Graf Date: Sat, 26 Oct 2024 18:50:09 +0200 Subject: [PATCH 4/6] feat: add moderationrequest endpoints --- sw360/moderationrequests.py | 95 +++++++++++ sw360/sw360_api.py | 4 +- tests/test_sw360_moderationrequests.py | 224 +++++++++++++++++++++++++ 3 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 sw360/moderationrequests.py create mode 100644 tests/test_sw360_moderationrequests.py diff --git a/sw360/moderationrequests.py b/sw360/moderationrequests.py new file mode 100644 index 0000000..0142548 --- /dev/null +++ b/sw360/moderationrequests.py @@ -0,0 +1,95 @@ +# ------------------------------------------------------------------------------- +# Copyright (c) 2024 Siemens +# All Rights Reserved. +# Authors: thomas.graf@siemens.com +# +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT +# ------------------------------------------------------------------------------- + +from typing import Any, Dict, List, Optional + +from .base import BaseMixin + + +class ModerationRequestMixin(BaseMixin): + def get_all_moderation_requests(self, page: int = -1, page_size: int = -1, + sort: str = "") -> List[Dict[str, Any]]: + """Get information of about all moderation requests + + API endpoint: GET /moderationrequest + + :param page: page to retrieve + :type page: int + :param page_size: page size to use + :type page_size: int + :param sort: sort order for the packages ("name,desc"; "name,asc") + :type sort: str + :return: list of moderation requests + :rtype: list of JSON moderation requests objects + :raises SW360Error: if there is a negative HTTP response + """ + + full_url = self.url + "resource/api/moderationrequest" + if page > -1: + full_url = self._add_param(full_url, "page=" + str(page)) + full_url = self._add_param(full_url, "page_entries=" + str(page_size)) + + if sort: + # ensure HTML encoding + sort = sort.replace(",", "%2C") + full_url = self._add_param(full_url, "sort=" + sort) + + resp = self.api_get(full_url) + return resp + + def get_moderation_requests_by_state(self, state: str, all_details: bool = False, + page: int = -1, page_size: int = -1, + sort: str = "") -> List[Dict[str, Any]]: + """Get information of about all moderation requests + + API endpoint: GET /moderationrequest/byState + + :param all_details: retrieve all package details (optional)) + :type all_details: bool + :param page: page to retrieve + :type page: int + :param page_size: page size to use + :type page_size: int + :param sort: sort order for the packages ("name,desc"; "name,asc") + :type sort: str + :return: list of moderation requests + :rtype: list of JSON moderation requests objects + :raises SW360Error: if there is a negative HTTP response + """ + + full_url = self.url + "resource/api/moderationrequest/byState?state=" + state + if all_details: + full_url = self._add_param(full_url, "allDetails=true") + + if page > -1: + full_url = self._add_param(full_url, "page=" + str(page)) + full_url = self._add_param(full_url, "page_entries=" + str(page_size)) + + if sort: + # ensure HTML encoding + sort = sort.replace(",", "%2C") + full_url = self._add_param(full_url, "sort=" + sort) + + resp = self.api_get(full_url) + return resp + + def get_moderation_request(self, mr_id: str) -> Optional[Dict[str, Any]]: + """Get information of about a moderation request + + API endpoint: GET /moderationrequest/{id} + + :param mr_id: the id of the moderation request to be requested + :type mr_id: string + :return: a moderation request + :rtype: JSON moderation request object + :raises SW360Error: if there is a negative HTTP response + """ + + resp = self.api_get(self.url + "resource/api/moderationrequest/" + mr_id) + return resp diff --git a/sw360/sw360_api.py b/sw360/sw360_api.py index 4603d18..9f93d36 100644 --- a/sw360/sw360_api.py +++ b/sw360/sw360_api.py @@ -25,6 +25,7 @@ from .sw360error import SW360Error from .vendor import VendorMixin from .vulnerabilities import VulnerabilitiesMixin +from .moderationrequests import ModerationRequestMixin # Retry mechanism for rate limiting adapter = HTTPAdapter(max_retries=Retry( @@ -48,7 +49,8 @@ class SW360( ReleasesMixin, VendorMixin, VulnerabilitiesMixin, - PackagesMixin + PackagesMixin, + ModerationRequestMixin ): """Python interface to the Siemens SW360 platform diff --git a/tests/test_sw360_moderationrequests.py b/tests/test_sw360_moderationrequests.py new file mode 100644 index 0000000..c052cfa --- /dev/null +++ b/tests/test_sw360_moderationrequests.py @@ -0,0 +1,224 @@ +# ------------------------------------------------------------------------------- +# Copyright (c) 2024 Siemens +# All Rights Reserved. +# Author: thomas.graf@siemens.com +# +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT +# ------------------------------------------------------------------------------- + +import sys +import unittest +import warnings + +import responses + +from sw360 import SW360 + +sys.path.insert(1, "..") + + +class Sw360TestModerationRequests(unittest.TestCase): + MYTOKEN = "MYTOKEN" + MYURL = "https://my.server.com/" + ERROR_MSG_NO_LOGIN = "Unable to login" + + def setUp(self) -> None: + warnings.filterwarnings( + "ignore", category=ResourceWarning, + message="unclosed.*") + + def _add_login_response(self) -> None: + """ + Add the response for a successful login. + """ + responses.add( + method=responses.GET, + url=self.MYURL + "resource/api/", + body="{'status': 'ok'}", + status=200, + content_type="application/json", + adding_headers={"Authorization": "Token " + self.MYTOKEN}, + ) + + @responses.activate + def test_get_all_moderation_requests(self) -> None: + lib = SW360(self.MYURL, self.MYTOKEN, False) + self._add_login_response() + actual = lib.login_api() + self.assertTrue(actual) + + responses.add( + method=responses.GET, + url=self.MYURL + "resource/api/moderationrequest?page=2&page_entries=8&sort=timestamp%2Cdesc", + body='''{ + "_embedded": { + "sw360:moderationRequests": [ + { + "id": "b41ddfb69b40439cabb18538313a036c", + "timestamp": 1729870771747, + "timestampOfDecision": 0, + "documentId": "8d101909d2f24ea592e666e9e9bb9eb0", + "documentType": "RELEASE", + "requestingUser": "gernot.hillier@siemens.com", + "moderators": ["admin2@sw360.org", "thomas.graf@siemens.com"], + "documentName": "@grpc/grpc-js (1.9.15)", + "moderationState": "PENDING", + "requestingUserDepartment": "SI", + "componentType": "OSS", + "moderatorsSize": 157, + "_links": { + "self": { + "href": "https://my.server.com/resource/api/moderationrequest/b4" + } + } + }] + }, + "_links": { + "first": { + "href": "https://my.server.com/resource/api/moderationrequest?page=0&page_entries=20" + }, + "next": { + "href": "https://my.server.com/resource/api/moderationrequest?page=1&page_entries=20" + }, + "last": { + "href": "https://my.server.com/resource/api/moderationrequest?page=2769&page_entries=20" + }, + "curies": [ + { + "href": "https://my.server.com/resource/docs/{rel}.html", + "name": "sw360", + "templated": true + } + ] + }, + "page": { + "size": 20, + "totalElements": 5555, + "totalPages": 3333, + "number": 0 + } + }''', + status=200, + content_type="application/json", + adding_headers={"Authorization": "Token " + self.MYTOKEN}, + ) + + mrs = lib.get_all_moderation_requests(page=2, page_size=8, sort="timestamp,desc") + self.assertIsNotNone(mrs) + self.assertTrue(len(mrs) > 0) + mr_list = mrs["_embedded"]["sw360:moderationRequests"] + self.assertEqual("@grpc/grpc-js (1.9.15)", mr_list[0]["documentName"]) + + @responses.activate + def test_get_moderation_requests_by_state(self) -> None: + lib = SW360(self.MYURL, self.MYTOKEN, False) + self._add_login_response() + actual = lib.login_api() + self.assertTrue(actual) + + responses.add( + method=responses.GET, + url=self.MYURL + "resource/api/moderationrequest/byState?state=open&allDetails=true&page=2&page_entries=8&sort=timestamp%2Cdesc", # noqa + body='''{ + "_embedded": { + "sw360:moderationRequests": [ + { + "id": "b41ddfb69b40439cabb18538313a036c", + "timestamp": 1729870771747, + "timestampOfDecision": 0, + "documentId": "8d101909d2f24ea592e666e9e9bb9eb0", + "documentType": "RELEASE", + "requestingUser": "gernot.hillier@siemens.com", + "moderators": ["admin2@sw360.org", "thomas.graf@siemens.com"], + "documentName": "@grpc/grpc-js (1.9.15)", + "moderationState": "PENDING", + "requestingUserDepartment": "SI", + "componentType": "OSS", + "moderatorsSize": 157, + "_links": { + "self": { + "href": "https://my.server.com/resource/api/moderationrequest/b4" + } + } + }] + }, + "_links": { + "first": { + "href": "https://my.server.com/resource/api/moderationrequest?page=0&page_entries=20" + }, + "next": { + "href": "https://my.server.com/resource/api/moderationrequest?page=1&page_entries=20" + }, + "last": { + "href": "https://my.server.com/resource/api/moderationrequest?page=2769&page_entries=20" + }, + "curies": [ + { + "href": "https://my.server.com/resource/docs/{rel}.html", + "name": "sw360", + "templated": true + } + ] + }, + "page": { + "size": 20, + "totalElements": 5555, + "totalPages": 3333, + "number": 0 + } + }''', + status=200, + content_type="application/json", + adding_headers={"Authorization": "Token " + self.MYTOKEN}, + ) + + mrs = lib.get_moderation_requests_by_state("open", True, page=2, page_size=8, sort="timestamp,desc") + self.assertIsNotNone(mrs) + self.assertTrue(len(mrs) > 0) + mr_list = mrs["_embedded"]["sw360:moderationRequests"] + self.assertEqual("@grpc/grpc-js (1.9.15)", mr_list[0]["documentName"]) + + @responses.activate + def test_get_license(self) -> None: + lib = SW360(self.MYURL, self.MYTOKEN, False) + self._add_login_response() + actual = lib.login_api() + self.assertTrue(actual) + + responses.add( + method=responses.GET, + url=self.MYURL + "resource/api/moderationrequest/0815", + body='''{ + "id": "0815", + "timestamp": 1729870771747, + "timestampOfDecision": 0, + "documentId": "8d101909d2f24ea592e666e9e9bb9eb0", + "documentType": "RELEASE", + "requestingUser": "gernot.hillier@siemens.com", + "moderators": ["admin2@sw360.org", "thomas.graf@siemens.com"], + "documentName": "@grpc/grpc-js (1.9.15)", + "moderationState": "PENDING", + "requestingUserDepartment": "SI", + "componentType": "OSS", + "moderatorsSize": 157, + "_links": { + "self": { + "href": "https://my.server.com/resource/api/moderationrequest/b41ddfb69b40439cabb18538313a036c" + } + } + }''', + status=200, + content_type="application/json", + adding_headers={"Authorization": "Token " + self.MYTOKEN}, + ) + + mr = lib.get_moderation_request("0815") + self.assertIsNotNone(license) + if mr: # only for mypy + self.assertEqual("b41ddfb69b40439cabb18538313a036c", mr["id"]) + self.assertEqual("@grpc/grpc-js (1.9.15)", mr["documentName"]) + + +if __name__ == "__main__": + unittest.main() From 3535ba87af1c36c79e777f6e7d10a6f0a3024134 Mon Sep 17 00:00:00 2001 From: Thomas Graf Date: Sat, 26 Oct 2024 18:57:06 +0200 Subject: [PATCH 5/6] fix: fix unit test --- tests/test_sw360_moderationrequests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sw360_moderationrequests.py b/tests/test_sw360_moderationrequests.py index c052cfa..d147844 100644 --- a/tests/test_sw360_moderationrequests.py +++ b/tests/test_sw360_moderationrequests.py @@ -216,7 +216,7 @@ def test_get_license(self) -> None: mr = lib.get_moderation_request("0815") self.assertIsNotNone(license) if mr: # only for mypy - self.assertEqual("b41ddfb69b40439cabb18538313a036c", mr["id"]) + self.assertEqual("0815", mr["id"]) self.assertEqual("@grpc/grpc-js (1.9.15)", mr["documentName"]) From dfb32f799124bcf42a001624d9ff1375e6452ae3 Mon Sep 17 00:00:00 2001 From: Thomas Graf Date: Sat, 26 Oct 2024 18:57:29 +0200 Subject: [PATCH 6/6] chores: small code changes to improve code coverage --- sw360/attachments.py | 12 +++--------- sw360/license.py | 12 +++--------- sw360/vendor.py | 12 +++--------- 3 files changed, 9 insertions(+), 27 deletions(-) diff --git a/sw360/attachments.py b/sw360/attachments.py index 11997fe..35106f3 100644 --- a/sw360/attachments.py +++ b/sw360/attachments.py @@ -55,16 +55,10 @@ def get_attachment_infos_for_resource(self, resource_type: str, resource_id: str + "/attachments" ) - if not resp: - return [] + if resp and "_embedded" in resp and "sw360:attachments" in resp["_embedded"]: + return resp["_embedded"]["sw360:attachments"] - if "_embedded" not in resp: - return [] - - if "sw360:attachments" not in resp["_embedded"]: - return [] - - return resp["_embedded"]["sw360:attachments"] + return [] def get_attachment_infos_for_release(self, release_id: str) -> List[Dict[str, Any]]: """Get information about the attachments of a release diff --git a/sw360/license.py b/sw360/license.py index 6b845da..4ba489a 100644 --- a/sw360/license.py +++ b/sw360/license.py @@ -117,16 +117,10 @@ def get_all_licenses(self) -> List[Dict[str, Any]]: """ resp = self.api_get(self.url + "resource/api/licenses") - if not resp: - return [] + if resp and "_embedded" in resp and "sw360:licenses" in resp["_embedded"]: + return resp["_embedded"]["sw360:licenses"] - if "_embedded" not in resp: - return [] - - if "sw360:licenses" not in resp["_embedded"]: - return [] - - return resp["_embedded"]["sw360:licenses"] + return [] def get_license(self, license_id: str) -> Optional[Dict[str, Any]]: """Get information of about a license diff --git a/sw360/vendor.py b/sw360/vendor.py index db24e5c..e710ee0 100644 --- a/sw360/vendor.py +++ b/sw360/vendor.py @@ -27,16 +27,10 @@ def get_all_vendors(self) -> List[Dict[str, Any]]: """ resp = self.api_get(self.url + "resource/api/vendors") - if not resp: - return [] + if resp and "_embedded" in resp and "sw360:vendors" in resp["_embedded"]: + return resp["_embedded"]["sw360:vendors"] - if "_embedded" not in resp: - return [] - - if "sw360:vendors" not in resp["_embedded"]: - return [] - - return resp["_embedded"]["sw360:vendors"] + return [] def get_vendor(self, vendor_id: str) -> Optional[Dict[str, Any]]: """Returns a vendor