Skip to content

Commit 4b6ea3c

Browse files
authored
Merge pull request #33 from sw360/32-add-package-api-support
add package api support
2 parents 99b2bcc + fce1f52 commit 4b6ea3c

12 files changed

Lines changed: 805 additions & 6 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ Test.tar.gz
2020
Test_file.zip
2121
test_all_components.json
2222
test_all_releases.json
23+
test_all_packages.json
24+
test_all_packages_with_details.json
2325
real_instance_tests.py
26+
sbom_to_packages.py
2427

2528
# test coverage
2629
htmlcov/

ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55

66
# SW360 Base Library for Python
77

8+
## NEXT (V1.6.0)
9+
10+
* packages REST API calls implemented.
11+
* unit test `test_login_failed_invalid_url` disabled because it delays all tests.
12+
* have unit tests for packages.
13+
814
## V1.5.1
915

1016
* update requests 2.31.0 => 2.32.2 to fix CVE-2024-35195.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,4 @@ warn_unused_ignores = true
7272
no_implicit_reexport = true
7373

7474
[tool.codespell]
75-
skip = "test_all_components.json,test_all_releases.json,./htmlcov/*,./__internal__/*,./docs/_static/*,./docs/searchindex.js"
75+
skip = "test_all_components.json,test_all_releases.json,./htmlcov/*,./__internal__/*,./docs/_static/*,./docs/searchindex.js,./docs/objects.inv"

sw360/base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def api_post(
136136

137137
raise SW360Error(response, url)
138138

139-
def api_patch(self, url: str = "", json: Dict[str, Any] = {}) -> Optional[Dict[str, Any]]:
139+
def api_patch(self, url: str = "", json: Any = {}) -> Optional[Dict[str, Any]]:
140140
"""
141141
Send a PATCH request to the specified URL with the provided json data.
142142
@@ -160,7 +160,10 @@ def api_patch(self, url: str = "", json: Dict[str, Any] = {}) -> Optional[Dict[s
160160
if response.ok:
161161
if response.status_code == 204: # 204 = no content
162162
return None
163-
return response.json()
163+
if response.content:
164+
return response.json()
165+
else:
166+
return None
164167

165168
raise SW360Error(response, url)
166169

sw360/packages.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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+
from .sw360error import SW360Error
14+
15+
16+
class PackagesMixin(BaseMixin):
17+
def get_package(self, package_id: str) -> Optional[Dict[str, Any]]:
18+
"""Get information of about a package
19+
20+
API endpoint: GET /package/{id}
21+
22+
:param package_id: the id of the package to be requested
23+
:type package_id: string
24+
:return: a package
25+
:rtype: JSON package object
26+
:raises SW360Error: if there is a negative HTTP response
27+
"""
28+
resp = self.api_get(self.url + "resource/api/packages/" + package_id)
29+
return resp
30+
31+
def get_packages_by_name(self, name: str) -> List[Any]:
32+
"""Gets a list of packages that match the given name.
33+
34+
API endpoint: GET /packages?name=
35+
36+
:param name: the name
37+
:type name: string
38+
:return: list of packages
39+
:rtype: list of JSON package objects
40+
:raises SW360Error: if there is a negative HTTP response
41+
"""
42+
full_url = self.url + "resource/api/packages?name=" + name
43+
resp = self.api_get(full_url)
44+
if resp and ("_embedded" in resp) and ("sw360:packages" in resp["_embedded"]):
45+
return resp["_embedded"]["sw360:packages"]
46+
47+
return []
48+
49+
# return type List[Dict[str, Any]] | Optional[Dict[str, Any]] for Python 3.11 is good,
50+
# Union[List[Dict[str, Any]], Optional[Dict[str, Any]]] for lower Python versions is not good
51+
def get_all_packages(self, fields: str = "", all_details: bool = False, page: int = -1,
52+
page_size: int = -1, sort: str = "") -> Any:
53+
"""Get information of about all packages
54+
55+
API endpoint: GET /releases
56+
57+
:param all_details: retrieve all package details (optional))
58+
:type all_details: bool
59+
:param page: page to retrieve
60+
:type page: int
61+
:param page_size: page size to use
62+
:type page_size: int
63+
:param sort: sort order for the packages ("name,desc"; "name,asc")
64+
:type sort: str
65+
:return: list of packages
66+
:rtype: list of JSON package objects
67+
:raises SW360Error: if there is a negative HTTP response
68+
"""
69+
full_url = self.url + "resource/api/packages"
70+
if all_details:
71+
full_url = self._add_param(full_url, "allDetails=true")
72+
73+
if fields:
74+
full_url = self._add_param(full_url, "fields=" + fields)
75+
76+
if page > -1:
77+
full_url = self._add_param(full_url, "page=" + str(page))
78+
full_url = self._add_param(full_url, "page_entries=" + str(page_size))
79+
80+
if sort:
81+
# ensure HTML encoding
82+
sort = sort.replace(",", "%2C")
83+
full_url = self._add_param(full_url, "sort=" + sort)
84+
85+
resp = self.api_get(full_url)
86+
87+
if page == -1 and resp and ("_embedded" in resp) and ("sw360:packages" in resp["_embedded"]):
88+
return resp["_embedded"]["sw360:packages"]
89+
90+
return resp
91+
92+
def get_packages_by_packagemanager(self, manager: str, page: int = -1,
93+
page_size: int = -1, sort: str = "") -> Any:
94+
"""Get information of about all packages of a specific package manager
95+
96+
API endpoint: GET /releases
97+
98+
:param manager: name of the package manager
99+
:type manager: str
100+
:param page: page to retrieve
101+
:type page: int
102+
:param page_size: page size to use
103+
:type page_size: int
104+
:param sort: sort order for the packages ("name,desc"; "name,asc")
105+
:type sort: str
106+
:return: list of packages
107+
:rtype: list of JSON package objects
108+
:raises SW360Error: if there is a negative HTTP response
109+
"""
110+
full_url = self.url + "resource/api/packages"
111+
full_url = self._add_param(full_url, "packageManager=" + str(manager))
112+
113+
if page > -1:
114+
full_url = self._add_param(full_url, "page=" + str(page))
115+
full_url = self._add_param(full_url, "page_entries=" + str(page_size))
116+
117+
if sort:
118+
# ensure HTML encoding
119+
sort = sort.replace(",", "%2C")
120+
full_url = self._add_param(full_url, "sort=" + sort)
121+
122+
resp = self.api_get(full_url)
123+
124+
if page == -1 and resp and ("_embedded" in resp) and ("sw360:packages" in resp["_embedded"]):
125+
return resp["_embedded"]["sw360:packages"]
126+
127+
return resp
128+
129+
def create_new_package(self, name: str, version: str, purl: str,
130+
package_type: str, package_details: Dict[str, Any] = {}) -> Optional[Dict[str, Any]]:
131+
"""Create a new package
132+
133+
API endpoint: POST /packages
134+
135+
:param name: name of new package (usually set to component name)
136+
:param version: version string of new package (e.g. "1.0")
137+
:param purl: purl / package-url of the package
138+
:param package_type: CycloneDX package type of the package
139+
:param package_details: further package details as defined by SW360 REST API
140+
:type name: string
141+
:type version: string
142+
:type purl: string
143+
:type package_type: string
144+
:type package_details: dict
145+
:return: SW360 result
146+
:rtype: JSON SW360 result object
147+
:raises SW360Error: if there is a negative HTTP response
148+
"""
149+
150+
for param in "name", "version":
151+
package_details[param] = locals()[param]
152+
package_details["purl"] = purl
153+
package_details["packageType"] = package_type
154+
155+
url = self.url + "resource/api/packages"
156+
response = self.api_post(
157+
url, json=package_details)
158+
if response is not None:
159+
if response.ok:
160+
return response.json()
161+
return None
162+
163+
def update_package(self, package: Dict[str, Any], package_id: str) -> Optional[Dict[str, Any]]:
164+
"""Update an existing package
165+
166+
API endpoint: PATCH /packages
167+
168+
:param release: the new package data
169+
:param release_id: the id of the package to be updated
170+
:type package: JSON
171+
:type package_id: string
172+
:return: SW360 result
173+
:rtype: JSON SW360 result object
174+
:raises SW360Error: if there is a negative HTTP response
175+
"""
176+
177+
if not package_id:
178+
raise SW360Error(message="No package id provided!")
179+
180+
url = self.url + "resource/api/packages/" + package_id
181+
return self.api_patch(url, json=package)
182+
183+
def delete_package(self, package_id: str) -> Optional[Dict[str, Any]]:
184+
"""Delete an existing package
185+
186+
API endpoint: DELETE /packages
187+
188+
:param package_id: the id of the package to be deleted
189+
:type package_id: string
190+
:return: SW360 result
191+
:rtype: JSON SW360 result object
192+
:raises SW360Error: if there is a negative HTTP response
193+
"""
194+
195+
if not package_id:
196+
raise SW360Error(message="No package id provided!")
197+
198+
url = self.url + "resource/api/packages/" + package_id
199+
response = self.api_delete(url)
200+
if response is not None:
201+
if response.ok:
202+
if response.text:
203+
return response.json()
204+
return None

sw360/project.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,39 @@ def update_project_release_relationship(
518518

519519
url = self.url + "resource/api/projects/" + project_id + "/release/" + release_id
520520
return self.api_patch(url, json=relation)
521+
522+
def link_packages_to_project(self, project_id: str, packages: List[str]) -> Optional[Dict[str, Any]]:
523+
"""Link (new) packages to a given project.
524+
525+
API endpoint PATCH /projects/{pid}/packages{rid}
526+
527+
:param project_id: the id of the existing project
528+
:type project_id: string
529+
:param packages: list of package ids
530+
:type packages: list of string
531+
:rtype: JSON SW360 result object
532+
:raises SW360Error: if the project id is missing ir there is a negative HTTP response
533+
"""
534+
if not project_id:
535+
raise SW360Error(message="No project id provided!")
536+
537+
url = self.url + "resource/api/projects/" + project_id + "/link/packages/"
538+
return self.api_patch(url, json=packages)
539+
540+
def unlink_packages_from_project(self, project_id: str, packages: List[str]) -> Optional[Dict[str, Any]]:
541+
"""Unlink packages from a given project.
542+
543+
API endpoint PATCH /projects/{pid}/packages{rid}
544+
545+
:param project_id: the id of the existing project
546+
:type project_id: string
547+
:param packages: list of package ids
548+
:type packages: list of string
549+
:rtype: JSON SW360 result object
550+
:raises SW360Error: if the project id is missing ir there is a negative HTTP response
551+
"""
552+
if not project_id:
553+
raise SW360Error(message="No project id provided!")
554+
555+
url = self.url + "resource/api/projects/" + project_id + "/unlink/packages/"
556+
return self.api_patch(url, json=packages)

sw360/releases.py

Lines changed: 37 additions & 1 deletion
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
@@ -250,3 +250,39 @@ def get_users_of_release(self, release_id: str) -> Optional[Dict[str, Any]]:
250250

251251
resp = self.api_get(self.url + "resource/api/releases/usedBy/" + release_id)
252252
return resp
253+
254+
def link_packages_to_release(self, release_id: str, packages: List[str]) -> Optional[Dict[str, Any]]:
255+
"""Link (new) packages to a given release.
256+
257+
API endpoint PATCH /release/{pid}/packages{rid}
258+
259+
:param release_id: the id of the existing release
260+
:type release_id: string
261+
:param packages: list of package ids
262+
:type packages: list of string
263+
:rtype: JSON SW360 result object
264+
:raises SW360Error: if the release id is missing ir there is a negative HTTP response
265+
"""
266+
if not release_id:
267+
raise SW360Error(message="No release id provided!")
268+
269+
url = self.url + "resource/api/releases/" + release_id + "/link/packages/"
270+
return self.api_patch(url, json=packages)
271+
272+
def unlink_packages_from_release(self, release_id: str, packages: List[str]) -> Optional[Dict[str, Any]]:
273+
"""Unlink packages from a given release.
274+
275+
API endpoint PATCH /release/{pid}/packages{rid}
276+
277+
:param release_id: the id of the existing release
278+
:type release_id: string
279+
:param packages: list of package ids
280+
:type packages: list of string
281+
:rtype: JSON SW360 result object
282+
:raises SW360Error: if the project id is missing ir there is a negative HTTP response
283+
"""
284+
if not release_id:
285+
raise SW360Error(message="No release id provided!")
286+
287+
url = self.url + "resource/api/releases/" + release_id + "/unlink/packages/"
288+
return self.api_patch(url, json=packages)

sw360/sw360_api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .clearing import ClearingMixin
2020
from .components import ComponentsMixin
2121
from .license import LicenseMixin
22+
from .packages import PackagesMixin
2223
from .project import ProjectMixin
2324
from .releases import ReleasesMixin
2425
from .sw360error import SW360Error
@@ -46,7 +47,8 @@ class SW360(
4647
ProjectMixin,
4748
ReleasesMixin,
4849
VendorMixin,
49-
VulnerabilitiesMixin
50+
VulnerabilitiesMixin,
51+
PackagesMixin
5052
):
5153
"""Python interface to the Siemens SW360 platform
5254

tests/test_sw360_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def test_login(self) -> None:
5656
actual = lib.login_api()
5757
self.assertTrue(actual)
5858

59-
def test_login_failed_invalid_url(self) -> None:
59+
def x_test_login_failed_invalid_url(self) -> None:
6060
lib = SW360(self.MYURL, self.MYTOKEN, False)
6161

6262
have_backup = False

0 commit comments

Comments
 (0)