Skip to content

Commit 5894329

Browse files
committed
feat: add return_type dict (#153)
Fixes #118
1 parent 33cc4f7 commit 5894329

File tree

7 files changed

+131
-30
lines changed

7 files changed

+131
-30
lines changed

src/re3data/_client/_async.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from __future__ import annotations
66

77
import logging
8-
from typing import TYPE_CHECKING, Literal, overload
8+
from typing import TYPE_CHECKING, Any, Literal, overload
99

1010
import httpx
1111

@@ -19,6 +19,7 @@
1919
)
2020
from re3data._exceptions import RepositoryNotFoundError
2121
from re3data._response import Response, _build_response, _parse_repositories_response, _parse_repository_response
22+
from re3data._serializer import _to_dict
2223

2324
if TYPE_CHECKING:
2425
from re3data._resources import Repository, RepositorySummary
@@ -46,16 +47,16 @@ async def async_log_response(response: httpx.Response) -> None:
4647
@overload
4748
def _dispatch_return_type(
4849
response: Response, resource_type: Literal[ResourceType.REPOSITORY], return_type: ReturnType
49-
) -> Repository | Response | str: ...
50+
) -> Repository | Response | dict[str, Any] | str: ...
5051
@overload
5152
def _dispatch_return_type(
5253
response: Response, resource_type: Literal[ResourceType.REPOSITORY_LIST], return_type: ReturnType
53-
) -> list[RepositorySummary] | Response | str: ...
54+
) -> list[RepositorySummary] | Response | dict[str, Any] | str: ...
5455

5556

5657
def _dispatch_return_type(
5758
response: Response, resource_type: ResourceType, return_type: ReturnType
58-
) -> Repository | list[RepositorySummary] | Response | str:
59+
) -> Repository | list[RepositorySummary] | Response | dict[str, Any] | str:
5960
"""Dispatch the response to the correct return type based on the provided return type and resource type.
6061
6162
Args:
@@ -65,16 +66,22 @@ def _dispatch_return_type(
6566
6667
Returns:
6768
Depending on the return_type and resource_type, this can be a Repository object, a list of RepositorySummary
68-
objects, an HTTP response, or the original XML.
69+
objects, an HTTP response, a dictionary representation or the original XML.
6970
"""
70-
if return_type == ReturnType.DATACLASS:
71-
if resource_type == ResourceType.REPOSITORY_LIST:
72-
return _parse_repositories_response(response)
73-
if resource_type == ResourceType.REPOSITORY:
74-
return _parse_repository_response(response)
71+
if return_type == ReturnType.RESPONSE:
72+
return response
7573
if return_type == ReturnType.XML:
7674
return response.text
77-
return response
75+
76+
parsed: Repository | list[RepositorySummary]
77+
if resource_type == ResourceType.REPOSITORY_LIST:
78+
parsed = _parse_repositories_response(response)
79+
if resource_type == ResourceType.REPOSITORY:
80+
parsed = _parse_repository_response(response)
81+
82+
if return_type == ReturnType.DATACLASS:
83+
return parsed
84+
return _to_dict(parsed)
7885

7986

8087
class AsyncRepositoryManager:
@@ -89,7 +96,7 @@ def __init__(self, client: AsyncClient) -> None:
8996

9097
async def list(
9198
self, query: str | None = None, return_type: ReturnType = ReturnType.DATACLASS
92-
) -> list[RepositorySummary] | Response | str:
99+
) -> list[RepositorySummary] | Response | dict[str, Any] | str:
93100
"""List the metadata of all repositories in the re3data API.
94101
95102
Args:
@@ -99,7 +106,7 @@ async def list(
99106
100107
Returns:
101108
Depending on the `return_type`, this can be a list of RepositorySummary objects, an HTTP response,
102-
or the original XML.
109+
a dictionary representation or the original XML.
103110
104111
Raises:
105112
ValueError: If an invalid `return_type` is provided.
@@ -112,15 +119,16 @@ async def list(
112119

113120
async def get(
114121
self, repository_id: str, return_type: ReturnType = ReturnType.DATACLASS
115-
) -> Repository | Response | str:
122+
) -> Repository | Response | dict[str, Any] | str:
116123
"""Get the metadata of a specific repository.
117124
118125
Args:
119126
repository_id: The identifier of the repository to retrieve.
120127
return_type: The desired return type for the API resource. Defaults to `ReturnType.DATACLASS`.
121128
122129
Returns:
123-
Depending on the `return_type`, this can be a Repository object, an HTTP response, or the original XML.
130+
Depending on the `return_type`, this can be a Repository object, an HTTP response,
131+
a dictionary representation or the original XML.
124132
125133
Raises:
126134
ValueError: If an invalid `return_type` is provided.

src/re3data/_client/_sync.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from __future__ import annotations
88

99
import logging
10-
from typing import TYPE_CHECKING, Literal, overload
10+
from typing import TYPE_CHECKING, Any, Literal, overload
1111

1212
import httpx
1313

@@ -21,6 +21,7 @@
2121
)
2222
from re3data._exceptions import RepositoryNotFoundError
2323
from re3data._response import Response, _build_response, _parse_repositories_response, _parse_repository_response
24+
from re3data._serializer import _to_dict
2425

2526
if TYPE_CHECKING:
2627
from re3data._resources import Repository, RepositorySummary
@@ -48,16 +49,16 @@ def log_response(response: httpx.Response) -> None:
4849
@overload
4950
def _dispatch_return_type(
5051
response: Response, resource_type: Literal[ResourceType.REPOSITORY], return_type: ReturnType
51-
) -> Repository | Response | str: ...
52+
) -> Repository | Response | dict[str, Any] | str: ...
5253
@overload
5354
def _dispatch_return_type(
5455
response: Response, resource_type: Literal[ResourceType.REPOSITORY_LIST], return_type: ReturnType
55-
) -> list[RepositorySummary] | Response | str: ...
56+
) -> list[RepositorySummary] | Response | dict[str, Any] | str: ...
5657

5758

5859
def _dispatch_return_type(
5960
response: Response, resource_type: ResourceType, return_type: ReturnType
60-
) -> Repository | list[RepositorySummary] | Response | str:
61+
) -> Repository | list[RepositorySummary] | Response | dict[str, Any] | str:
6162
"""Dispatch the response to the correct return type based on the provided return type and resource type.
6263
6364
Args:
@@ -67,16 +68,22 @@ def _dispatch_return_type(
6768
6869
Returns:
6970
Depending on the return_type and resource_type, this can be a Repository object, a list of RepositorySummary
70-
objects, an HTTP response, or the original XML.
71+
objects, an HTTP response, a dictionary representation or the original XML.
7172
"""
72-
if return_type == ReturnType.DATACLASS:
73-
if resource_type == ResourceType.REPOSITORY_LIST:
74-
return _parse_repositories_response(response)
75-
if resource_type == ResourceType.REPOSITORY:
76-
return _parse_repository_response(response)
73+
if return_type == ReturnType.RESPONSE:
74+
return response
7775
if return_type == ReturnType.XML:
7876
return response.text
79-
return response
77+
78+
parsed: Repository | list[RepositorySummary]
79+
if resource_type == ResourceType.REPOSITORY_LIST:
80+
parsed = _parse_repositories_response(response)
81+
if resource_type == ResourceType.REPOSITORY:
82+
parsed = _parse_repository_response(response)
83+
84+
if return_type == ReturnType.DATACLASS:
85+
return parsed
86+
return _to_dict(parsed)
8087

8188

8289
class RepositoryManager:
@@ -91,7 +98,7 @@ def __init__(self, client: Client) -> None:
9198

9299
def list(
93100
self, query: str | None = None, return_type: ReturnType = ReturnType.DATACLASS
94-
) -> list[RepositorySummary] | Response | str:
101+
) -> list[RepositorySummary] | Response | dict[str, Any] | str:
95102
"""List the metadata of all repositories in the re3data API.
96103
97104
Args:
@@ -101,7 +108,7 @@ def list(
101108
102109
Returns:
103110
Depending on the `return_type`, this can be a list of RepositorySummary objects, an HTTP response,
104-
or the original XML.
111+
a dictionary representation or the original XML.
105112
106113
Raises:
107114
ValueError: If an invalid `return_type` is provided.
@@ -112,15 +119,18 @@ def list(
112119
response = self._client._request(Endpoint.REPOSITORY_LIST.value, query_params)
113120
return _dispatch_return_type(response, ResourceType.REPOSITORY_LIST, return_type)
114121

115-
def get(self, repository_id: str, return_type: ReturnType = ReturnType.DATACLASS) -> Repository | Response | str:
122+
def get(
123+
self, repository_id: str, return_type: ReturnType = ReturnType.DATACLASS
124+
) -> Repository | Response | dict[str, Any] | str:
116125
"""Get the metadata of a specific repository.
117126
118127
Args:
119128
repository_id: The identifier of the repository to retrieve.
120129
return_type: The desired return type for the API resource. Defaults to `ReturnType.DATACLASS`.
121130
122131
Returns:
123-
Depending on the `return_type`, this can be a Repository object, an HTTP response, or the original XML.
132+
Depending on the `return_type`, this can be a Repository object, an HTTP response,
133+
a dictionary representation or the original XML.
124134
125135
Raises:
126136
ValueError: If an invalid `return_type` is provided.

src/re3data/_client/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class ResourceType(str, Enum):
3333

3434
class ReturnType(str, Enum):
3535
DATACLASS = "dataclass"
36+
DICT = "dict"
3637
RESPONSE = "response"
3738
XML = "xml"
3839

src/re3data/_serializer.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# SPDX-FileCopyrightText: 2024 Heinz-Alexander Fütterer
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""The _serializer module offers functions for converting parsed data into dictionaries.
6+
7+
This module provides functions to serialize various types of data into dictionaries.
8+
The serialized data can be used for further processing or storage.
9+
10+
Functions:
11+
_to_dict: Serialize parsed data into a dictionary.
12+
"""
13+
14+
from typing import Any
15+
16+
from xsdata.formats.dataclass.context import XmlContext
17+
from xsdata.formats.dataclass.serializers import DictEncoder
18+
from xsdata.formats.dataclass.serializers.config import SerializerConfig
19+
20+
from re3data._resources import Repository, RepositorySummary
21+
22+
CONFIG = SerializerConfig(indent=" ")
23+
CONTEXT = XmlContext()
24+
25+
DICT_ENCODER = DictEncoder(context=CONTEXT, config=CONFIG)
26+
27+
28+
def _to_dict(parsed: Repository | list[RepositorySummary]) -> dict[str, Any]:
29+
"""Serialize parsed data into a dictionary.
30+
31+
Args:
32+
parsed: The input data to be serialized. It can be either a single `Repository` object or a list of
33+
`RepositorySummary` objects.
34+
35+
Returns:
36+
dict[str, Any]: A dictionary representation of the input data.
37+
"""
38+
return DICT_ENCODER.encode(parsed) # type: ignore[no-any-return]

tests/integration/test_async_client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ async def test_client_list_repositories_xml(async_client: AsyncClient, mock_repo
4949
assert "<repository>" in response
5050

5151

52+
async def test_client_list_repositories_dict(async_client: AsyncClient, mock_repository_list_route: Route) -> None:
53+
response = await async_client.repositories.list(return_type=ReturnType.DICT)
54+
assert isinstance(response, list)
55+
repository = response[0]
56+
assert isinstance(repository, dict)
57+
assert repository["id"] == "r3d100010371"
58+
59+
5260
async def test_client_list_repositories_response(async_client: AsyncClient, mock_repository_list_route: Route) -> None:
5361
response = await async_client.repositories.list(return_type=ReturnType.RESPONSE)
5462
assert isinstance(response, Response)
@@ -93,6 +101,14 @@ async def test_client_get_single_repository_xml(
93101
assert "<r3d:re3data.orgIdentifier>r3d100010468</r3d:re3data.orgIdentifier>" in response
94102

95103

104+
async def test_client_get_single_repository_dict(
105+
async_client: AsyncClient, mock_repository_get_route: Route, zenodo_id: str
106+
) -> None:
107+
response = await async_client.repositories.get(zenodo_id, return_type=ReturnType.DICT)
108+
assert isinstance(response, dict)
109+
assert response["re3data.orgIdentifier"] == zenodo_id
110+
111+
96112
async def test_client_get_single_repository_response(
97113
async_client: AsyncClient, mock_repository_get_route: Route, zenodo_id: str
98114
) -> None:

tests/integration/test_cli.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ def test_repository_list_response(mock_repository_list_route: Route) -> None:
9595
assert "url=URL('https://www.re3data.org/api/beta/repositories')" in result.output
9696

9797

98+
def test_repository_list_dict(mock_repository_list_route: Route) -> None:
99+
result = runner.invoke(app, ["repository", "list", "--return-type", "dict"])
100+
assert result.exit_code == 0
101+
assert "'id': 'r3d100010371'," in result.output
102+
assert "'doi': 'https://doi.org/10.17616/R3P594'," in result.output
103+
104+
98105
def test_repository_list_invalid_return_type(mock_repository_list_route: Route) -> None:
99106
result = runner.invoke(app, ["repository", "list", "--return-type", "json"])
100107
assert result.exit_code == 2
@@ -143,6 +150,13 @@ def test_repository_get_with_repository_id_xml(mock_repository_get_route: Route,
143150
assert "<r3d:re3data.orgIdentifier>r3d100010468" in result.output
144151

145152

153+
def test_repository_get_with_repository_id_dict(mock_repository_get_route: Route, zenodo_id: str) -> None:
154+
result = runner.invoke(app, ["repository", "get", zenodo_id, "--return-type", "dict"])
155+
assert result.exit_code == 0
156+
assert "{" in result.output
157+
assert "'re3data.orgIdentifier': 'r3d100010468'," in result.output
158+
159+
146160
def test_repository_get_with_repository_id_response(mock_repository_get_route: Route, zenodo_id: str) -> None:
147161
result = runner.invoke(app, ["repository", "get", zenodo_id, "--return-type", "response"])
148162
assert result.exit_code == 0

tests/integration/test_client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ def test_client_list_repositories_xml(client: Client, mock_repository_list_route
4949
assert "<repository>" in response
5050

5151

52+
def test_client_list_repositories_dict(client: Client, mock_repository_list_route: Route) -> None:
53+
response = client.repositories.list(return_type=ReturnType.DICT)
54+
assert isinstance(response, list)
55+
repository = response[0]
56+
assert isinstance(repository, dict)
57+
assert repository["id"] == "r3d100010371"
58+
59+
5260
def test_client_list_repositories_response(client: Client, mock_repository_list_route: Route) -> None:
5361
response = client.repositories.list(return_type=ReturnType.RESPONSE)
5462
assert isinstance(response, Response)
@@ -89,6 +97,12 @@ def test_client_get_single_repository_xml(client: Client, mock_repository_get_ro
8997
assert "<r3d:re3data.orgIdentifier>r3d100010468</r3d:re3data.orgIdentifier>" in response
9098

9199

100+
def test_client_get_single_repository_dict(client: Client, mock_repository_get_route: Route, zenodo_id: str) -> None:
101+
response = client.repositories.get(zenodo_id, return_type=ReturnType.DICT)
102+
assert isinstance(response, dict)
103+
assert response["re3data.orgIdentifier"] == zenodo_id
104+
105+
92106
def test_client_get_single_repository_response(
93107
client: Client, mock_repository_get_route: Route, zenodo_id: str
94108
) -> None:

0 commit comments

Comments
 (0)