Skip to content

Commit 0694afb

Browse files
committed
Improve returned objects of Metadata API methods
Return dataclasses instead of untyped dicts.
1 parent 6d1197a commit 0694afb

File tree

2 files changed

+79
-38
lines changed

2 files changed

+79
-38
lines changed

gvm/protocols/http/openvasd/_metadata.py

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,44 @@
66
API wrapper for retrieving metadata from the openvasd HTTP API using HEAD requests.
77
"""
88

9+
from dataclasses import dataclass
910
from typing import Union
1011

1112
import httpx
1213

1314
from ._api import OpenvasdAPI
1415

1516

17+
@dataclass
18+
class Metadata:
19+
"""
20+
Represents metadata returned by the openvasd API.
21+
22+
Attributes:
23+
api_version: Comma separated list of available API versions
24+
feed_version: Version of the feed.
25+
authentication: Supported authentication methods
26+
"""
27+
28+
api_version: str
29+
feed_version: str
30+
authentication: str
31+
32+
33+
@dataclass
34+
class MetadataError:
35+
"""
36+
Represents an error response from the metadata API.
37+
38+
Attributes:
39+
error: Error message.
40+
status_code: HTTP status code of the error response.
41+
"""
42+
43+
error: str
44+
status_code: int
45+
46+
1647
class MetadataAPI(OpenvasdAPI):
1748
"""
1849
Provides access to metadata endpoints exposed by the openvasd server
@@ -27,20 +58,12 @@ class MetadataAPI(OpenvasdAPI):
2758
is handled gracefully.
2859
"""
2960

30-
def get(self) -> dict[str, Union[str, int]]:
61+
def get(self) -> Union[Metadata, MetadataError]:
3162
"""
3263
Perform a HEAD request to `/` to retrieve top-level API metadata.
3364
3465
Returns:
35-
A dictionary containing:
36-
37-
- "api-version"
38-
- "feed-version"
39-
- "authentication"
40-
41-
Or if exceptions are suppressed and error occurs:
42-
43-
- {"error": str, "status_code": int}
66+
A Metadata instance or MetadataError if exceptions are suppressed and an error occurs.
4467
4568
Raises:
4669
httpx.HTTPStatusError: For non-401 HTTP errors if exceptions are not suppressed.
@@ -50,30 +73,24 @@ def get(self) -> dict[str, Union[str, int]]:
5073
try:
5174
response = self._client.head("/")
5275
response.raise_for_status()
53-
return {
54-
"api-version": response.headers.get("api-version"),
55-
"feed-version": response.headers.get("feed-version"),
56-
"authentication": response.headers.get("authentication"),
57-
}
76+
return Metadata(
77+
api_version=response.headers.get("api-version"),
78+
feed_version=response.headers.get("feed-version"),
79+
authentication=response.headers.get("authentication"),
80+
)
5881
except httpx.HTTPStatusError as e:
5982
if self._suppress_exceptions:
60-
return {"error": str(e), "status_code": e.response.status_code}
83+
return MetadataError(
84+
error=str(e), status_code=e.response.status_code
85+
)
6186
raise
6287

63-
def get_scans(self) -> dict[str, Union[str, int]]:
88+
def get_scans(self) -> Union[Metadata, MetadataError]:
6489
"""
65-
Perform a HEAD request to `/scans` to retrieve scan endpoint metadata.
90+
Perform a HEAD request to `/scans` to retrieve scan endpoint metadata.
6691
6792
Returns:
68-
A dictionary containing:
69-
70-
- "api-version"
71-
- "feed-version"
72-
- "authentication"
73-
74-
Or if safe=True and error occurs:
75-
76-
- {"error": str, "status_code": int}
93+
A Metadata instance or MetadataError if exceptions are suppressed and an error occurs.
7794
7895
Raises:
7996
httpx.HTTPStatusError: For non-401 HTTP errors if exceptions are not suppressed.
@@ -83,12 +100,14 @@ def get_scans(self) -> dict[str, Union[str, int]]:
83100
try:
84101
response = self._client.head("/scans")
85102
response.raise_for_status()
86-
return {
87-
"api-version": response.headers.get("api-version"),
88-
"feed-version": response.headers.get("feed-version"),
89-
"authentication": response.headers.get("authentication"),
90-
}
103+
return Metadata(
104+
api_version=response.headers.get("api-version"),
105+
feed_version=response.headers.get("feed-version"),
106+
authentication=response.headers.get("authentication"),
107+
)
91108
except httpx.HTTPStatusError as e:
92109
if self._suppress_exceptions:
93-
return {"error": str(e), "status_code": e.response.status_code}
110+
return MetadataError(
111+
error=str(e), status_code=e.response.status_code
112+
)
94113
raise

tests/protocols/http/openvasd/test_metadata.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77

88
import httpx
99

10-
from gvm.protocols.http.openvasd._metadata import MetadataAPI
10+
from gvm.protocols.http.openvasd._metadata import (
11+
Metadata,
12+
MetadataAPI,
13+
MetadataError,
14+
)
1115

1216

1317
def _mock_head_response(status_code=200, headers=None):
@@ -35,7 +39,14 @@ def test_get_successful(self):
3539
result = api.get()
3640
self.mock_client.head.assert_called_once_with("/")
3741
mock_response.raise_for_status.assert_called_once()
38-
self.assertEqual(result, headers)
42+
self.assertEqual(
43+
result,
44+
Metadata(
45+
api_version="1.0",
46+
feed_version="2025.01",
47+
authentication="API-KEY",
48+
),
49+
)
3950

4051
def test_get_unauthorized_with_suppress_exceptions(self):
4152
mock_response = MagicMock(spec=httpx.Response)
@@ -47,7 +58,9 @@ def test_get_unauthorized_with_suppress_exceptions(self):
4758

4859
api = MetadataAPI(self.mock_client, suppress_exceptions=True)
4960
result = api.get()
50-
self.assertEqual(result, {"error": "Unauthorized", "status_code": 401})
61+
self.assertEqual(
62+
result, MetadataError(error="Unauthorized", status_code=401)
63+
)
5164

5265
def test_get_failure_raises_httpx_error(self):
5366
mock_response = _mock_head_response(500, None)
@@ -77,7 +90,14 @@ def test_get_scans_successful(self):
7790
result = api.get_scans()
7891
self.mock_client.head.assert_called_once_with("/scans")
7992
mock_response.raise_for_status.assert_called_once()
80-
self.assertEqual(result, headers)
93+
self.assertEqual(
94+
result,
95+
Metadata(
96+
api_version="1.0",
97+
feed_version="2025.01",
98+
authentication="API-KEY",
99+
),
100+
)
81101

82102
def test_get_scans_unauthorized_with_suppress_exceptions(self):
83103
mock_response = MagicMock(spec=httpx.Response)
@@ -89,7 +109,9 @@ def test_get_scans_unauthorized_with_suppress_exceptions(self):
89109

90110
api = MetadataAPI(self.mock_client, suppress_exceptions=True)
91111
result = api.get_scans()
92-
self.assertEqual(result, {"error": "Unauthorized", "status_code": 401})
112+
self.assertEqual(
113+
result, MetadataError(error="Unauthorized", status_code=401)
114+
)
93115

94116
def test_get_scans_failure_raises_httpx_error(self):
95117
mock_response = _mock_head_response(500, None)

0 commit comments

Comments
 (0)