Skip to content

Commit

Permalink
feat: add return_type json (#156)
Browse files Browse the repository at this point in the history
Fixes #119
  • Loading branch information
afuetterer committed Jul 3, 2024
1 parent 5d1084e commit f948d6b
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 97 deletions.
46 changes: 3 additions & 43 deletions src/re3data/_client/_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any, Literal, overload
from typing import TYPE_CHECKING, Any

import httpx

Expand All @@ -15,11 +15,11 @@
ResourceType,
ReturnType,
_build_query_params,
_dispatch_return_type,
is_valid_return_type,
)
from re3data._exceptions import RepositoryNotFoundError
from re3data._response import Response, _build_response, _parse_repositories_response, _parse_repository_response
from re3data._serializer import _to_dict
from re3data._response import Response, _build_response

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


@overload
def _dispatch_return_type(
response: Response, resource_type: Literal[ResourceType.REPOSITORY], return_type: ReturnType
) -> Repository | Response | dict[str, Any] | str: ...
@overload
def _dispatch_return_type(
response: Response, resource_type: Literal[ResourceType.REPOSITORY_LIST], return_type: ReturnType
) -> list[RepositorySummary] | Response | dict[str, Any] | str: ...


def _dispatch_return_type(
response: Response, resource_type: ResourceType, return_type: ReturnType
) -> Repository | list[RepositorySummary] | Response | dict[str, Any] | str:
"""Dispatch the response to the correct return type based on the provided return type and resource type.
Args:
response: The response object.
resource_type: The type of resource being processed.
return_type: The desired return type for the API resource.
Returns:
Depending on the return_type and resource_type, this can be a Repository object, a list of RepositorySummary
objects, an HTTP response, a dictionary representation or the original XML.
"""
if return_type == ReturnType.RESPONSE:
return response
if return_type == ReturnType.XML:
return response.text

parsed: Repository | list[RepositorySummary]
if resource_type == ResourceType.REPOSITORY_LIST:
parsed = _parse_repositories_response(response)
if resource_type == ResourceType.REPOSITORY:
parsed = _parse_repository_response(response)

if return_type == ReturnType.DATACLASS:
return parsed
return _to_dict(parsed)


class AsyncRepositoryManager:
"""A manager for interacting with repositories in the re3data API.
Expand Down
46 changes: 3 additions & 43 deletions src/re3data/_client/_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any, Literal, overload
from typing import TYPE_CHECKING, Any

import httpx

Expand All @@ -17,11 +17,11 @@
ResourceType,
ReturnType,
_build_query_params,
_dispatch_return_type,
is_valid_return_type,
)
from re3data._exceptions import RepositoryNotFoundError
from re3data._response import Response, _build_response, _parse_repositories_response, _parse_repository_response
from re3data._serializer import _to_dict
from re3data._response import Response, _build_response

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


@overload
def _dispatch_return_type(
response: Response, resource_type: Literal[ResourceType.REPOSITORY], return_type: ReturnType
) -> Repository | Response | dict[str, Any] | str: ...
@overload
def _dispatch_return_type(
response: Response, resource_type: Literal[ResourceType.REPOSITORY_LIST], return_type: ReturnType
) -> list[RepositorySummary] | Response | dict[str, Any] | str: ...


def _dispatch_return_type(
response: Response, resource_type: ResourceType, return_type: ReturnType
) -> Repository | list[RepositorySummary] | Response | dict[str, Any] | str:
"""Dispatch the response to the correct return type based on the provided return type and resource type.
Args:
response: The response object.
resource_type: The type of resource being processed.
return_type: The desired return type for the API resource.
Returns:
Depending on the return_type and resource_type, this can be a Repository object, a list of RepositorySummary
objects, an HTTP response, a dictionary representation or the original XML.
"""
if return_type == ReturnType.RESPONSE:
return response
if return_type == ReturnType.XML:
return response.text

parsed: Repository | list[RepositorySummary]
if resource_type == ResourceType.REPOSITORY_LIST:
parsed = _parse_repositories_response(response)
if resource_type == ResourceType.REPOSITORY:
parsed = _parse_repository_response(response)

if return_type == ReturnType.DATACLASS:
return parsed
return _to_dict(parsed)


class RepositoryManager:
"""A manager for interacting with repositories in the re3data API.
Expand Down
54 changes: 51 additions & 3 deletions src/re3data/_client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@
#
# SPDX-License-Identifier: MIT

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

from __future__ import annotations

from enum import Enum
from typing import Any
from typing import TYPE_CHECKING, Any, Literal, overload

import httpx

from re3data import __version__
from re3data._response import Response, _parse_repositories_response, _parse_repository_response
from re3data._serializer import _to_dict, _to_json

if TYPE_CHECKING:
from re3data._resources import Repository, RepositorySummary

BASE_URL: str = "https://www.re3data.org/api/beta/"
DEFAULT_HEADERS: dict[str, str] = {
Expand All @@ -34,6 +39,7 @@ class ResourceType(str, Enum):
class ReturnType(str, Enum):
DATACLASS = "dataclass"
DICT = "dict"
JSON = "json"
RESPONSE = "response"
XML = "xml"

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


@overload
def _dispatch_return_type(
response: Response, resource_type: Literal[ResourceType.REPOSITORY], return_type: ReturnType
) -> Repository | Response | dict[str, Any] | str: ...
@overload
def _dispatch_return_type(
response: Response, resource_type: Literal[ResourceType.REPOSITORY_LIST], return_type: ReturnType
) -> list[RepositorySummary] | Response | dict[str, Any] | str: ...


def _dispatch_return_type(
response: Response, resource_type: ResourceType, return_type: ReturnType
) -> Repository | list[RepositorySummary] | Response | dict[str, Any] | str:
"""Dispatch the response to the correct return type based on the provided return type and resource type.
Args:
response: The response object.
resource_type: The type of resource being processed.
return_type: The desired return type for the API resource.
Returns:
Depending on the return_type and resource_type, this can be a Repository object, a list of RepositorySummary
objects, an HTTP response, a dictionary representation or the original XML.
"""
if return_type == ReturnType.RESPONSE:
return response
if return_type == ReturnType.XML:
return response.text

parsed: Repository | list[RepositorySummary]
if resource_type == ResourceType.REPOSITORY_LIST:
parsed = _parse_repositories_response(response)
if resource_type == ResourceType.REPOSITORY:
parsed = _parse_repository_response(response)
if return_type == ReturnType.DATACLASS:
return parsed

if return_type == ReturnType.JSON:
return _to_json(parsed)
return _to_dict(parsed)


class BaseClient:
"""An abstract base class for clients that interact with the re3data API."""
"""A base class for clients that interact with the re3data API."""

def __init__(
self,
Expand Down
23 changes: 19 additions & 4 deletions src/re3data/_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@
#
# SPDX-License-Identifier: MIT

"""The _serializer module offers functions for converting parsed data into dictionaries.
"""The _serializer module offers functions for converting parsed data into dictionaries or JSON strings.
This module provides functions to serialize various types of data into dictionaries.
This module provides functions to serialize various types of data into dictionaries or JSON strings.
The serialized data can be used for further processing or storage.
Functions:
_to_dict: Serialize parsed data into a dictionary.
_to_json: Serialize parsed data into a JSON string.
"""

from typing import Any

from xsdata.formats.dataclass.context import XmlContext
from xsdata.formats.dataclass.serializers import DictEncoder
from xsdata.formats.dataclass.serializers import DictEncoder, JsonSerializer
from xsdata.formats.dataclass.serializers.config import SerializerConfig

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

DICT_ENCODER = DictEncoder(context=CONTEXT, config=CONFIG)
JSON_SERIALIZER = JsonSerializer(context=CONTEXT, config=CONFIG)


def _to_dict(parsed: Repository | list[RepositorySummary]) -> dict[str, Any]:
Expand All @@ -33,6 +35,19 @@ def _to_dict(parsed: Repository | list[RepositorySummary]) -> dict[str, Any]:
`RepositorySummary` objects.
Returns:
dict[str, Any]: A dictionary representation of the input data.
A dictionary representation of the input data.
"""
return DICT_ENCODER.encode(parsed) # type: ignore[no-any-return]


def _to_json(parsed: Repository | list[RepositorySummary]) -> str:
"""Serialize parsed data into a JSON string.
Args:
parsed: The input data to be serialized. It can be either a single `Repository` object or a list of
`RepositorySummary` objects.
Returns:
A JSON representation of the input data.
"""
return JSON_SERIALIZER.render(parsed)
16 changes: 16 additions & 0 deletions tests/integration/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ async def test_client_list_repositories_xml(async_client: AsyncClient, mock_repo
assert "<repository>" in response


async def test_client_list_repositories_json(async_client: AsyncClient, mock_repository_list_route: Route) -> None:
response = await async_client.repositories.list(return_type=ReturnType.JSON)
assert isinstance(response, str)
assert '"id": "r3d100010371",' in response
assert '"doi": "https://doi.org/10.17616/R3P594",' in response


async def test_client_list_repositories_dict(async_client: AsyncClient, mock_repository_list_route: Route) -> None:
response = await async_client.repositories.list(return_type=ReturnType.DICT)
assert isinstance(response, list)
Expand Down Expand Up @@ -101,6 +108,15 @@ async def test_client_get_single_repository_xml(
assert "<r3d:re3data.orgIdentifier>r3d100010468</r3d:re3data.orgIdentifier>" in response


async def test_client_get_single_repository_json(
async_client: AsyncClient, mock_repository_get_route: Route, zenodo_id: str
) -> None:
response = await async_client.repositories.get(zenodo_id, return_type=ReturnType.JSON)
assert isinstance(response, str)
assert "{" in response
assert '"re3data.orgIdentifier": "r3d100010468",' in response


async def test_client_get_single_repository_dict(
async_client: AsyncClient, mock_repository_get_route: Route, zenodo_id: str
) -> None:
Expand Down
22 changes: 18 additions & 4 deletions tests/integration/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ def test_repository_list_xml(mock_repository_list_route: Route) -> None:
assert "<doi>https://doi.org/10.17616/R3P594</doi>" in result.output


def test_repository_list_json(mock_repository_list_route: Route) -> None:
result = runner.invoke(app, ["repository", "list", "--return-type", "json"])
assert result.exit_code == 0
assert '"id": "r3d100010371",' in result.output
assert '"doi": "https://doi.org/10.17616/R3P594",' in result.output


def test_repository_list_response(mock_repository_list_route: Route) -> None:
result = runner.invoke(app, ["repository", "list", "--return-type", "response"])
assert result.exit_code == 0
Expand All @@ -103,10 +110,10 @@ def test_repository_list_dict(mock_repository_list_route: Route) -> None:


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


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


def test_repository_get_with_repository_id_json(mock_repository_get_route: Route, zenodo_id: str) -> None:
result = runner.invoke(app, ["repository", "get", zenodo_id, "--return-type", "json"])
assert result.exit_code == 0
assert "{" in result.output
assert '"re3data.orgIdentifier": "r3d100010468",' in result.output


def test_repository_get_with_repository_id_dict(mock_repository_get_route: Route, zenodo_id: str) -> None:
result = runner.invoke(app, ["repository", "get", zenodo_id, "--return-type", "dict"])
assert result.exit_code == 0
Expand All @@ -165,10 +179,10 @@ def test_repository_get_with_repository_id_response(mock_repository_get_route: R


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


@pytest.mark.default_cassette("repository.yaml")
Expand Down
14 changes: 14 additions & 0 deletions tests/integration/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ def test_client_list_repositories_xml(client: Client, mock_repository_list_route
assert "<repository>" in response


def test_client_list_repositories_json(client: Client, mock_repository_list_route: Route) -> None:
response = client.repositories.list(return_type=ReturnType.JSON)
assert isinstance(response, str)
assert '"id": "r3d100010371",' in response
assert '"doi": "https://doi.org/10.17616/R3P594",' in response


def test_client_list_repositories_dict(client: Client, mock_repository_list_route: Route) -> None:
response = client.repositories.list(return_type=ReturnType.DICT)
assert isinstance(response, list)
Expand Down Expand Up @@ -97,6 +104,13 @@ def test_client_get_single_repository_xml(client: Client, mock_repository_get_ro
assert "<r3d:re3data.orgIdentifier>r3d100010468</r3d:re3data.orgIdentifier>" in response


def test_client_get_single_repository_json(client: Client, mock_repository_get_route: Route, zenodo_id: str) -> None:
response = client.repositories.get(zenodo_id, return_type=ReturnType.JSON)
assert isinstance(response, str)
assert "{" in response
assert '"re3data.orgIdentifier": "r3d100010468",' in response


def test_client_get_single_repository_dict(client: Client, mock_repository_get_route: Route, zenodo_id: str) -> None:
response = client.repositories.get(zenodo_id, return_type=ReturnType.DICT)
assert isinstance(response, dict)
Expand Down

0 comments on commit f948d6b

Please sign in to comment.