Skip to content

Commit 19d0cba

Browse files
authored
Merge pull request #36 from sw360/35-add-recentreleases-endpoint
add recentreleases endpoint
2 parents 92702fc + dfb32f7 commit 19d0cba

11 files changed

Lines changed: 499 additions & 41 deletions

ChangeLog.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55

66
# SW360 Base Library for Python
77

8+
## NEXT
9+
10+
* more REST API endpoints implemented:
11+
* `get_recent_releases`
12+
* `get_recent_components`
13+
* `get_all_moderation_requests`
14+
* `get_moderation_requests_by_state`
15+
* `get_moderation_request`
16+
817
## V1.6.0
918

1019
* packages REST API calls implemented.
@@ -21,7 +30,7 @@
2130

2231
* when using CaPyCLI in a CI pipeline, connection problems to the SW360 server (5xx) cause
2332
the pipeline to fail. We have now add an improved session handling to all api requests.
24-
* dependency updates due to security vulnerabilities in idna.
33+
* dependency updates due to security vulnerabilities in `idna`.
2534

2635
## V1.4.1
2736

sw360/attachments.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,10 @@ def get_attachment_infos_for_resource(self, resource_type: str, resource_id: str
5555
+ "/attachments"
5656
)
5757

58-
if not resp:
59-
return []
58+
if resp and "_embedded" in resp and "sw360:attachments" in resp["_embedded"]:
59+
return resp["_embedded"]["sw360:attachments"]
6060

61-
if "_embedded" not in resp:
62-
return []
63-
64-
if "sw360:attachments" not in resp["_embedded"]:
65-
return []
66-
67-
return resp["_embedded"]["sw360:attachments"]
61+
return []
6862

6963
def get_attachment_infos_for_release(self, release_id: str) -> List[Dict[str, Any]]:
7064
"""Get information about the attachments of a release

sw360/components.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -------------------------------------------------------------------------------
2-
# Copyright (c) 2019-2023 Siemens
2+
# Copyright (c) 2019-2024 Siemens
33
# Copyright (c) 2022 BMW CarIT GmbH
44
# All Rights Reserved.
55
# Authors: thomas.graf@siemens.com, gernot.hillier@siemens.com
@@ -84,16 +84,10 @@ def get_components_by_type(self, component_type: str) -> List[Dict[str, Any]]:
8484
"""
8585

8686
resp = self.api_get(self.url + "resource/api/components?type=" + component_type)
87-
if not resp:
88-
return []
89-
90-
if "_embedded" not in resp:
91-
return []
92-
93-
if "sw360:components" not in resp["_embedded"]:
94-
return []
87+
if resp and ("_embedded" in resp) and ("sw360:components" in resp["_embedded"]):
88+
return resp["_embedded"]["sw360:components"]
9589

96-
return resp["_embedded"]["sw360:components"]
90+
return []
9791

9892
def get_component(self, component_id: str) -> Optional[Dict[str, Any]]:
9993
"""Get information of about a component
@@ -292,3 +286,19 @@ def get_users_of_component(self, component_id: str) -> Optional[Dict[str, Any]]:
292286

293287
resp = self.api_get(self.url + "resource/api/components/usedBy/" + component_id)
294288
return resp
289+
290+
def get_recent_components(self) -> Optional[Dict[str, Any]]:
291+
"""Get 5 of the service's most recently created components.
292+
293+
API endpoint: GET /components/recentComponents
294+
295+
:return: a list of components
296+
:rtype: JSON list of component objects
297+
:raises SW360Error: if there is a negative HTTP response
298+
"""
299+
url = self.url + "resource/api/components/recentComponents"
300+
resp = self.api_get(url)
301+
if resp and ("_embedded" in resp) and ("sw360:components" in resp["_embedded"]):
302+
return resp["_embedded"]["sw360:components"]
303+
304+
return resp

sw360/license.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,10 @@ def get_all_licenses(self) -> List[Dict[str, Any]]:
117117
"""
118118

119119
resp = self.api_get(self.url + "resource/api/licenses")
120-
if not resp:
121-
return []
120+
if resp and "_embedded" in resp and "sw360:licenses" in resp["_embedded"]:
121+
return resp["_embedded"]["sw360:licenses"]
122122

123-
if "_embedded" not in resp:
124-
return []
125-
126-
if "sw360:licenses" not in resp["_embedded"]:
127-
return []
128-
129-
return resp["_embedded"]["sw360:licenses"]
123+
return []
130124

131125
def get_license(self, license_id: str) -> Optional[Dict[str, Any]]:
132126
"""Get information of about a license

sw360/moderationrequests.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# -------------------------------------------------------------------------------
2+
# Copyright (c) 2024 Siemens
3+
# All Rights Reserved.
4+
# Authors: thomas.graf@siemens.com
5+
#
6+
# Licensed under the MIT license.
7+
# SPDX-License-Identifier: MIT
8+
# -------------------------------------------------------------------------------
9+
10+
from typing import Any, Dict, List, Optional
11+
12+
from .base import BaseMixin
13+
14+
15+
class ModerationRequestMixin(BaseMixin):
16+
def get_all_moderation_requests(self, page: int = -1, page_size: int = -1,
17+
sort: str = "") -> List[Dict[str, Any]]:
18+
"""Get information of about all moderation requests
19+
20+
API endpoint: GET /moderationrequest
21+
22+
:param page: page to retrieve
23+
:type page: int
24+
:param page_size: page size to use
25+
:type page_size: int
26+
:param sort: sort order for the packages ("name,desc"; "name,asc")
27+
:type sort: str
28+
:return: list of moderation requests
29+
:rtype: list of JSON moderation requests objects
30+
:raises SW360Error: if there is a negative HTTP response
31+
"""
32+
33+
full_url = self.url + "resource/api/moderationrequest"
34+
if page > -1:
35+
full_url = self._add_param(full_url, "page=" + str(page))
36+
full_url = self._add_param(full_url, "page_entries=" + str(page_size))
37+
38+
if sort:
39+
# ensure HTML encoding
40+
sort = sort.replace(",", "%2C")
41+
full_url = self._add_param(full_url, "sort=" + sort)
42+
43+
resp = self.api_get(full_url)
44+
return resp
45+
46+
def get_moderation_requests_by_state(self, state: str, all_details: bool = False,
47+
page: int = -1, page_size: int = -1,
48+
sort: str = "") -> List[Dict[str, Any]]:
49+
"""Get information of about all moderation requests
50+
51+
API endpoint: GET /moderationrequest/byState
52+
53+
:param all_details: retrieve all package details (optional))
54+
:type all_details: bool
55+
:param page: page to retrieve
56+
:type page: int
57+
:param page_size: page size to use
58+
:type page_size: int
59+
:param sort: sort order for the packages ("name,desc"; "name,asc")
60+
:type sort: str
61+
:return: list of moderation requests
62+
:rtype: list of JSON moderation requests objects
63+
:raises SW360Error: if there is a negative HTTP response
64+
"""
65+
66+
full_url = self.url + "resource/api/moderationrequest/byState?state=" + state
67+
if all_details:
68+
full_url = self._add_param(full_url, "allDetails=true")
69+
70+
if page > -1:
71+
full_url = self._add_param(full_url, "page=" + str(page))
72+
full_url = self._add_param(full_url, "page_entries=" + str(page_size))
73+
74+
if sort:
75+
# ensure HTML encoding
76+
sort = sort.replace(",", "%2C")
77+
full_url = self._add_param(full_url, "sort=" + sort)
78+
79+
resp = self.api_get(full_url)
80+
return resp
81+
82+
def get_moderation_request(self, mr_id: str) -> Optional[Dict[str, Any]]:
83+
"""Get information of about a moderation request
84+
85+
API endpoint: GET /moderationrequest/{id}
86+
87+
:param mr_id: the id of the moderation request to be requested
88+
:type mr_id: string
89+
:return: a moderation request
90+
:rtype: JSON moderation request object
91+
:raises SW360Error: if there is a negative HTTP response
92+
"""
93+
94+
resp = self.api_get(self.url + "resource/api/moderationrequest/" + mr_id)
95+
return resp

sw360/releases.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,19 @@ def unlink_packages_from_release(self, release_id: str, packages: List[str]) ->
286286

287287
url = self.url + "resource/api/releases/" + release_id + "/unlink/packages/"
288288
return self.api_patch(url, json=packages)
289+
290+
def get_recent_releases(self) -> Optional[Dict[str, Any]]:
291+
"""Get 5 of the service's most recently created releases.
292+
293+
API endpoint: GET /releases/recentReleases
294+
295+
:return: a list of releases
296+
:rtype: JSON list of release objects
297+
:raises SW360Error: if there is a negative HTTP response
298+
"""
299+
url = self.url + "resource/api/releases/recentReleases"
300+
resp = self.api_get(url)
301+
if resp and ("_embedded" in resp) and ("sw360:releases" in resp["_embedded"]):
302+
return resp["_embedded"]["sw360:releases"]
303+
304+
return resp

sw360/sw360_api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .sw360error import SW360Error
2626
from .vendor import VendorMixin
2727
from .vulnerabilities import VulnerabilitiesMixin
28+
from .moderationrequests import ModerationRequestMixin
2829

2930
# Retry mechanism for rate limiting
3031
adapter = HTTPAdapter(max_retries=Retry(
@@ -48,7 +49,8 @@ class SW360(
4849
ReleasesMixin,
4950
VendorMixin,
5051
VulnerabilitiesMixin,
51-
PackagesMixin
52+
PackagesMixin,
53+
ModerationRequestMixin
5254
):
5355
"""Python interface to the Siemens SW360 platform
5456

sw360/vendor.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,10 @@ def get_all_vendors(self) -> List[Dict[str, Any]]:
2727
"""
2828

2929
resp = self.api_get(self.url + "resource/api/vendors")
30-
if not resp:
31-
return []
30+
if resp and "_embedded" in resp and "sw360:vendors" in resp["_embedded"]:
31+
return resp["_embedded"]["sw360:vendors"]
3232

33-
if "_embedded" not in resp:
34-
return []
35-
36-
if "sw360:vendors" not in resp["_embedded"]:
37-
return []
38-
39-
return resp["_embedded"]["sw360:vendors"]
33+
return []
4034

4135
def get_vendor(self, vendor_id: str) -> Optional[Dict[str, Any]]:
4236
"""Returns a vendor

tests/test_sw360_components.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -839,11 +839,73 @@ def xtest_get_component_real(self) -> None:
839839
lib.login_api()
840840
# c = lib.get_component("eaba2f0416e000e8ca5b2ccb4400633e")
841841
# c = lib.get_components_by_external_id( "package-url", "pkg:nuget/Tethys.Logging")
842-
c = lib.api_get("https://sw360.siemens.com/resource/api/components/searchByExternalIds?package-url=pkg:nuget/Tethys.Logging") # noqa
842+
c = lib.api_get("https://my.server.com/resource/api/components/searchByExternalIds?package-url=pkg:nuget/Tethys.Logging") # noqa
843843
print(c)
844844

845+
@responses.activate
846+
def test_get_recent_components(self) -> None:
847+
lib = SW360(self.MYURL, self.MYTOKEN, False)
848+
lib.force_no_session = True
849+
self._add_login_response()
850+
actual = lib.login_api()
851+
self.assertTrue(actual)
852+
853+
responses.add(
854+
method=responses.GET,
855+
url=self.MYURL + "resource/api/components/recentComponents",
856+
body='''{
857+
"_embedded": {
858+
"sw360:components": [
859+
{
860+
"id": "ff6f1b5b212b4f93b306e2cceca4f64d",
861+
"name": "intl-listformat",
862+
"description": "n/a",
863+
"componentType": "OSS",
864+
"visbility": "EVERYONE",
865+
"mainLicenseIds": [],
866+
"_links": {
867+
"self": {
868+
"href": "https://my.server.com/resource/api/components/ff"
869+
}
870+
}
871+
},
872+
{
873+
"id": "f916b35d6c864014bd8823c45615aeab",
874+
"name": "fields-metadata-plugin",
875+
"description": "n/a",
876+
"componentType": "OSS",
877+
"visbility": "EVERYONE",
878+
"mainLicenseIds": [],
879+
"_links": {
880+
"self": {
881+
"href": "https://my.server.com/resource/api/components/f9"
882+
}
883+
}
884+
}
885+
]
886+
},
887+
"_links": {
888+
"curies": [
889+
{
890+
"href": "https://my.server.com/resource/docs/{rel}.html",
891+
"name": "sw360",
892+
"templated": true
893+
}
894+
]
895+
}
896+
}''',
897+
status=200,
898+
content_type="application/json",
899+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
900+
)
901+
902+
components = lib.get_recent_components()
903+
self.assertIsNotNone(components)
904+
self.assertEqual(2, len(components))
905+
self.assertEqual("intl-listformat", components[0]["name"])
906+
self.assertEqual("OSS", components[0]["componentType"])
907+
845908

846909
if __name__ == "__main__":
847-
# unittest.main()
848910
x = Sw360TestComponents()
849911
x.test_get_all_components_with_fields_and_paging()

0 commit comments

Comments
 (0)