Skip to content

Commit f948d6b

Browse files
committed
feat: add return_type json (#156)
Fixes #119
1 parent 5d1084e commit f948d6b

File tree

7 files changed

+124
-97
lines changed

7 files changed

+124
-97
lines changed

src/re3data/_client/_async.py

Lines changed: 3 additions & 43 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, Any, Literal, overload
8+
from typing import TYPE_CHECKING, Any
99

1010
import httpx
1111

@@ -15,11 +15,11 @@
1515
ResourceType,
1616
ReturnType,
1717
_build_query_params,
18+
_dispatch_return_type,
1819
is_valid_return_type,
1920
)
2021
from re3data._exceptions import RepositoryNotFoundError
21-
from re3data._response import Response, _build_response, _parse_repositories_response, _parse_repository_response
22-
from re3data._serializer import _to_dict
22+
from re3data._response import Response, _build_response
2323

2424
if TYPE_CHECKING:
2525
from re3data._resources import Repository, RepositorySummary
@@ -44,46 +44,6 @@ async def async_log_response(response: httpx.Response) -> None:
4444
)
4545

4646

47-
@overload
48-
def _dispatch_return_type(
49-
response: Response, resource_type: Literal[ResourceType.REPOSITORY], return_type: ReturnType
50-
) -> Repository | Response | dict[str, Any] | str: ...
51-
@overload
52-
def _dispatch_return_type(
53-
response: Response, resource_type: Literal[ResourceType.REPOSITORY_LIST], return_type: ReturnType
54-
) -> list[RepositorySummary] | Response | dict[str, Any] | str: ...
55-
56-
57-
def _dispatch_return_type(
58-
response: Response, resource_type: ResourceType, return_type: ReturnType
59-
) -> Repository | list[RepositorySummary] | Response | dict[str, Any] | str:
60-
"""Dispatch the response to the correct return type based on the provided return type and resource type.
61-
62-
Args:
63-
response: The response object.
64-
resource_type: The type of resource being processed.
65-
return_type: The desired return type for the API resource.
66-
67-
Returns:
68-
Depending on the return_type and resource_type, this can be a Repository object, a list of RepositorySummary
69-
objects, an HTTP response, a dictionary representation or the original XML.
70-
"""
71-
if return_type == ReturnType.RESPONSE:
72-
return response
73-
if return_type == ReturnType.XML:
74-
return response.text
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)
85-
86-
8747
class AsyncRepositoryManager:
8848
"""A manager for interacting with repositories in the re3data API.
8949

src/re3data/_client/_sync.py

Lines changed: 3 additions & 43 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, Any, Literal, overload
10+
from typing import TYPE_CHECKING, Any
1111

1212
import httpx
1313

@@ -17,11 +17,11 @@
1717
ResourceType,
1818
ReturnType,
1919
_build_query_params,
20+
_dispatch_return_type,
2021
is_valid_return_type,
2122
)
2223
from re3data._exceptions import RepositoryNotFoundError
23-
from re3data._response import Response, _build_response, _parse_repositories_response, _parse_repository_response
24-
from re3data._serializer import _to_dict
24+
from re3data._response import Response, _build_response
2525

2626
if TYPE_CHECKING:
2727
from re3data._resources import Repository, RepositorySummary
@@ -46,46 +46,6 @@ def log_response(response: httpx.Response) -> None:
4646
)
4747

4848

49-
@overload
50-
def _dispatch_return_type(
51-
response: Response, resource_type: Literal[ResourceType.REPOSITORY], return_type: ReturnType
52-
) -> Repository | Response | dict[str, Any] | str: ...
53-
@overload
54-
def _dispatch_return_type(
55-
response: Response, resource_type: Literal[ResourceType.REPOSITORY_LIST], return_type: ReturnType
56-
) -> list[RepositorySummary] | Response | dict[str, Any] | str: ...
57-
58-
59-
def _dispatch_return_type(
60-
response: Response, resource_type: ResourceType, return_type: ReturnType
61-
) -> Repository | list[RepositorySummary] | Response | dict[str, Any] | str:
62-
"""Dispatch the response to the correct return type based on the provided return type and resource type.
63-
64-
Args:
65-
response: The response object.
66-
resource_type: The type of resource being processed.
67-
return_type: The desired return type for the API resource.
68-
69-
Returns:
70-
Depending on the return_type and resource_type, this can be a Repository object, a list of RepositorySummary
71-
objects, an HTTP response, a dictionary representation or the original XML.
72-
"""
73-
if return_type == ReturnType.RESPONSE:
74-
return response
75-
if return_type == ReturnType.XML:
76-
return response.text
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)
87-
88-
8949
class RepositoryManager:
9050
"""A manager for interacting with repositories in the re3data API.
9151

src/re3data/_client/base.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22
#
33
# SPDX-License-Identifier: MIT
44

5-
"""The base module provides base class for clients to interact with the re3data API."""
5+
"""The base module provides a base class for clients to interact with the re3data API."""
66

77
from __future__ import annotations
88

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

1212
import httpx
1313

1414
from re3data import __version__
15+
from re3data._response import Response, _parse_repositories_response, _parse_repository_response
16+
from re3data._serializer import _to_dict, _to_json
17+
18+
if TYPE_CHECKING:
19+
from re3data._resources import Repository, RepositorySummary
1520

1621
BASE_URL: str = "https://www.re3data.org/api/beta/"
1722
DEFAULT_HEADERS: dict[str, str] = {
@@ -34,6 +39,7 @@ class ResourceType(str, Enum):
3439
class ReturnType(str, Enum):
3540
DATACLASS = "dataclass"
3641
DICT = "dict"
42+
JSON = "json"
3743
RESPONSE = "response"
3844
XML = "xml"
3945

@@ -71,8 +77,50 @@ def _build_query_params(query: str | None = None) -> dict[str, str]:
7177
return query_params
7278

7379

80+
@overload
81+
def _dispatch_return_type(
82+
response: Response, resource_type: Literal[ResourceType.REPOSITORY], return_type: ReturnType
83+
) -> Repository | Response | dict[str, Any] | str: ...
84+
@overload
85+
def _dispatch_return_type(
86+
response: Response, resource_type: Literal[ResourceType.REPOSITORY_LIST], return_type: ReturnType
87+
) -> list[RepositorySummary] | Response | dict[str, Any] | str: ...
88+
89+
90+
def _dispatch_return_type(
91+
response: Response, resource_type: ResourceType, return_type: ReturnType
92+
) -> Repository | list[RepositorySummary] | Response | dict[str, Any] | str:
93+
"""Dispatch the response to the correct return type based on the provided return type and resource type.
94+
95+
Args:
96+
response: The response object.
97+
resource_type: The type of resource being processed.
98+
return_type: The desired return type for the API resource.
99+
100+
Returns:
101+
Depending on the return_type and resource_type, this can be a Repository object, a list of RepositorySummary
102+
objects, an HTTP response, a dictionary representation or the original XML.
103+
"""
104+
if return_type == ReturnType.RESPONSE:
105+
return response
106+
if return_type == ReturnType.XML:
107+
return response.text
108+
109+
parsed: Repository | list[RepositorySummary]
110+
if resource_type == ResourceType.REPOSITORY_LIST:
111+
parsed = _parse_repositories_response(response)
112+
if resource_type == ResourceType.REPOSITORY:
113+
parsed = _parse_repository_response(response)
114+
if return_type == ReturnType.DATACLASS:
115+
return parsed
116+
117+
if return_type == ReturnType.JSON:
118+
return _to_json(parsed)
119+
return _to_dict(parsed)
120+
121+
74122
class BaseClient:
75-
"""An abstract base class for clients that interact with the re3data API."""
123+
"""A base class for clients that interact with the re3data API."""
76124

77125
def __init__(
78126
self,

src/re3data/_serializer.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@
22
#
33
# SPDX-License-Identifier: MIT
44

5-
"""The _serializer module offers functions for converting parsed data into dictionaries.
5+
"""The _serializer module offers functions for converting parsed data into dictionaries or JSON strings.
66
7-
This module provides functions to serialize various types of data into dictionaries.
7+
This module provides functions to serialize various types of data into dictionaries or JSON strings.
88
The serialized data can be used for further processing or storage.
99
1010
Functions:
1111
_to_dict: Serialize parsed data into a dictionary.
12+
_to_json: Serialize parsed data into a JSON string.
1213
"""
1314

1415
from typing import Any
1516

1617
from xsdata.formats.dataclass.context import XmlContext
17-
from xsdata.formats.dataclass.serializers import DictEncoder
18+
from xsdata.formats.dataclass.serializers import DictEncoder, JsonSerializer
1819
from xsdata.formats.dataclass.serializers.config import SerializerConfig
1920

2021
from re3data._resources import Repository, RepositorySummary
@@ -23,6 +24,7 @@
2324
CONTEXT = XmlContext()
2425

2526
DICT_ENCODER = DictEncoder(context=CONTEXT, config=CONFIG)
27+
JSON_SERIALIZER = JsonSerializer(context=CONTEXT, config=CONFIG)
2628

2729

2830
def _to_dict(parsed: Repository | list[RepositorySummary]) -> dict[str, Any]:
@@ -33,6 +35,19 @@ def _to_dict(parsed: Repository | list[RepositorySummary]) -> dict[str, Any]:
3335
`RepositorySummary` objects.
3436
3537
Returns:
36-
dict[str, Any]: A dictionary representation of the input data.
38+
A dictionary representation of the input data.
3739
"""
3840
return DICT_ENCODER.encode(parsed) # type: ignore[no-any-return]
41+
42+
43+
def _to_json(parsed: Repository | list[RepositorySummary]) -> str:
44+
"""Serialize parsed data into a JSON string.
45+
46+
Args:
47+
parsed: The input data to be serialized. It can be either a single `Repository` object or a list of
48+
`RepositorySummary` objects.
49+
50+
Returns:
51+
A JSON representation of the input data.
52+
"""
53+
return JSON_SERIALIZER.render(parsed)

tests/integration/test_async_client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ 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_json(async_client: AsyncClient, mock_repository_list_route: Route) -> None:
53+
response = await async_client.repositories.list(return_type=ReturnType.JSON)
54+
assert isinstance(response, str)
55+
assert '"id": "r3d100010371",' in response
56+
assert '"doi": "https://doi.org/10.17616/R3P594",' in response
57+
58+
5259
async def test_client_list_repositories_dict(async_client: AsyncClient, mock_repository_list_route: Route) -> None:
5360
response = await async_client.repositories.list(return_type=ReturnType.DICT)
5461
assert isinstance(response, list)
@@ -101,6 +108,15 @@ async def test_client_get_single_repository_xml(
101108
assert "<r3d:re3data.orgIdentifier>r3d100010468</r3d:re3data.orgIdentifier>" in response
102109

103110

111+
async def test_client_get_single_repository_json(
112+
async_client: AsyncClient, mock_repository_get_route: Route, zenodo_id: str
113+
) -> None:
114+
response = await async_client.repositories.get(zenodo_id, return_type=ReturnType.JSON)
115+
assert isinstance(response, str)
116+
assert "{" in response
117+
assert '"re3data.orgIdentifier": "r3d100010468",' in response
118+
119+
104120
async def test_client_get_single_repository_dict(
105121
async_client: AsyncClient, mock_repository_get_route: Route, zenodo_id: str
106122
) -> None:

tests/integration/test_cli.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ def test_repository_list_xml(mock_repository_list_route: Route) -> None:
8888
assert "<doi>https://doi.org/10.17616/R3P594</doi>" in result.output
8989

9090

91+
def test_repository_list_json(mock_repository_list_route: Route) -> None:
92+
result = runner.invoke(app, ["repository", "list", "--return-type", "json"])
93+
assert result.exit_code == 0
94+
assert '"id": "r3d100010371",' in result.output
95+
assert '"doi": "https://doi.org/10.17616/R3P594",' in result.output
96+
97+
9198
def test_repository_list_response(mock_repository_list_route: Route) -> None:
9299
result = runner.invoke(app, ["repository", "list", "--return-type", "response"])
93100
assert result.exit_code == 0
@@ -103,10 +110,10 @@ def test_repository_list_dict(mock_repository_list_route: Route) -> None:
103110

104111

105112
def test_repository_list_invalid_return_type(mock_repository_list_route: Route) -> None:
106-
result = runner.invoke(app, ["repository", "list", "--return-type", "json"])
113+
result = runner.invoke(app, ["repository", "list", "--return-type", "excel"])
107114
assert result.exit_code == 2
108115
assert "Error" in result.output
109-
assert "Invalid value for '--return-type': 'json'" in result.output
116+
assert "Invalid value for '--return-type': 'excel'" in result.output
110117

111118

112119
def test_repository_list_query(mock_repository_list_query_route: Route) -> None:
@@ -150,6 +157,13 @@ def test_repository_get_with_repository_id_xml(mock_repository_get_route: Route,
150157
assert "<r3d:re3data.orgIdentifier>r3d100010468" in result.output
151158

152159

160+
def test_repository_get_with_repository_id_json(mock_repository_get_route: Route, zenodo_id: str) -> None:
161+
result = runner.invoke(app, ["repository", "get", zenodo_id, "--return-type", "json"])
162+
assert result.exit_code == 0
163+
assert "{" in result.output
164+
assert '"re3data.orgIdentifier": "r3d100010468",' in result.output
165+
166+
153167
def test_repository_get_with_repository_id_dict(mock_repository_get_route: Route, zenodo_id: str) -> None:
154168
result = runner.invoke(app, ["repository", "get", zenodo_id, "--return-type", "dict"])
155169
assert result.exit_code == 0
@@ -165,10 +179,10 @@ def test_repository_get_with_repository_id_response(mock_repository_get_route: R
165179

166180

167181
def test_repository_get_with_repository_id_invalid_return_type(zenodo_id: str) -> None:
168-
result = runner.invoke(app, ["repository", "get", zenodo_id, "--return-type", "json"])
182+
result = runner.invoke(app, ["repository", "get", zenodo_id, "--return-type", "excel"])
169183
assert result.exit_code == 2
170184
assert "Error" in result.output
171-
assert "Invalid value for '--return-type': 'json'" in result.output
185+
assert "Invalid value for '--return-type': 'excel'" in result.output
172186

173187

174188
@pytest.mark.default_cassette("repository.yaml")

tests/integration/test_client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ 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_json(client: Client, mock_repository_list_route: Route) -> None:
53+
response = client.repositories.list(return_type=ReturnType.JSON)
54+
assert isinstance(response, str)
55+
assert '"id": "r3d100010371",' in response
56+
assert '"doi": "https://doi.org/10.17616/R3P594",' in response
57+
58+
5259
def test_client_list_repositories_dict(client: Client, mock_repository_list_route: Route) -> None:
5360
response = client.repositories.list(return_type=ReturnType.DICT)
5461
assert isinstance(response, list)
@@ -97,6 +104,13 @@ def test_client_get_single_repository_xml(client: Client, mock_repository_get_ro
97104
assert "<r3d:re3data.orgIdentifier>r3d100010468</r3d:re3data.orgIdentifier>" in response
98105

99106

107+
def test_client_get_single_repository_json(client: Client, mock_repository_get_route: Route, zenodo_id: str) -> None:
108+
response = client.repositories.get(zenodo_id, return_type=ReturnType.JSON)
109+
assert isinstance(response, str)
110+
assert "{" in response
111+
assert '"re3data.orgIdentifier": "r3d100010468",' in response
112+
113+
100114
def test_client_get_single_repository_dict(client: Client, mock_repository_get_route: Route, zenodo_id: str) -> None:
101115
response = client.repositories.get(zenodo_id, return_type=ReturnType.DICT)
102116
assert isinstance(response, dict)

0 commit comments

Comments
 (0)