diff --git a/src/re3data/_cli.py b/src/re3data/_cli.py
index 84d9a7b..0adcdd8 100644
--- a/src/re3data/_cli.py
+++ b/src/re3data/_cli.py
@@ -7,6 +7,7 @@
import logging
import sys
import typing
+from typing import Annotated, Optional
from rich.console import Console
@@ -70,9 +71,17 @@ def callback(
@repositories_app.command("list")
-def list_repositories(return_type: ReturnType = ReturnType.DATACLASS) -> None:
+def list_repositories(
+ query: Annotated[
+ Optional[str], # noqa: UP007
+ typer.Option(
+ help="A query to filter the results. If provided, only repositories matching the query will be returned."
+ ),
+ ] = None,
+ return_type: ReturnType = ReturnType.DATACLASS,
+) -> None:
"""List the metadata of all repositories in the re3data API."""
- response = re3data.repositories.list(return_type)
+ response = re3data.repositories.list(query, return_type)
console.print(response)
diff --git a/src/re3data/_client/_async.py b/src/re3data/_client/_async.py
index a1d0c98..9d41395 100644
--- a/src/re3data/_client/_async.py
+++ b/src/re3data/_client/_async.py
@@ -9,7 +9,14 @@
import httpx
-from re3data._client.base import BaseClient, Endpoint, ResourceType, ReturnType, is_valid_return_type
+from re3data._client.base import (
+ BaseClient,
+ Endpoint,
+ ResourceType,
+ ReturnType,
+ _build_query_params,
+ is_valid_return_type,
+)
from re3data._exceptions import RepositoryNotFoundError
from re3data._response import Response, _build_response, _parse_repositories_response, _parse_repository_response
@@ -80,10 +87,14 @@ class AsyncRepositoryManager:
def __init__(self, client: AsyncClient) -> None:
self._client = client
- async def list(self, return_type: ReturnType = ReturnType.DATACLASS) -> list[RepositorySummary] | Response | str:
+ async def list(
+ self, query: str | None = None, return_type: ReturnType = ReturnType.DATACLASS
+ ) -> list[RepositorySummary] | Response | str:
"""List the metadata of all repositories in the re3data API.
Args:
+ query: A query string to filter the results. If provided, only repositories matching the query
+ will be returned.
return_type: The desired return type for the API resource. Defaults to `ReturnType.DATACLASS`.
Returns:
@@ -95,7 +106,8 @@ async def list(self, return_type: ReturnType = ReturnType.DATACLASS) -> list[Rep
httpx.HTTPStatusError: If the server returned an error status code >= 500.
"""
is_valid_return_type(return_type)
- response = await self._client._request(Endpoint.REPOSITORY_LIST.value)
+ query_params = _build_query_params(query)
+ response = await self._client._request(Endpoint.REPOSITORY_LIST.value, query_params)
return _dispatch_return_type(response, ResourceType.REPOSITORY_LIST, return_type)
async def get(
@@ -135,6 +147,9 @@ class AsyncClient(BaseClient):
>>> response
[RepositorySummary(id='r3d100010468', doi='https://doi.org/10.17616/R3QP53', name='Zenodo', link=Link(href='https://www.re3data.org/api/beta/repository/r3d100010468', rel='self'))]
... (remaining repositories truncated)
+ >>> response = await async_client.repositories.list(query="biosharing")
+ >>> response
+ [RepositorySummary(id='r3d100010142', doi='https://doi.org/10.17616/R3WS3X', name='FAIRsharing', link=Link(href='https://www.re3data.org/api/beta/repository/r3d100010142', rel='self'))]
"""
_client: httpx.AsyncClient
@@ -144,11 +159,13 @@ def __init__(self) -> None:
self._client.event_hooks["response"] = [async_log_response]
self._repository_manager: AsyncRepositoryManager = AsyncRepositoryManager(self)
- async def _request(self, path: str) -> Response:
+ async def _request(self, path: str, query_params: dict[str, str] | None = None) -> Response:
"""Send a HTTP GET request to the specified API endpoint.
Args:
path: The path to send the request to.
+ query_params: Optional URL query parameters to be sent with the HTTP GET request. This dictionary
+ contains key-value pairs that will be added as query parameters to the API endpoint specified by path.
Returns:
The response object from the HTTP request.
@@ -157,7 +174,7 @@ async def _request(self, path: str) -> Response:
httpx.HTTPStatusError: If the server returned an error status code >= 500.
RepositoryNotFoundError: If the `repository_id` is not found.
"""
- http_response = await self._client.get(path)
+ http_response = await self._client.get(path, params=query_params)
if http_response.is_server_error:
http_response.raise_for_status()
return _build_response(http_response)
diff --git a/src/re3data/_client/_sync.py b/src/re3data/_client/_sync.py
index d5a390a..5b9aba6 100644
--- a/src/re3data/_client/_sync.py
+++ b/src/re3data/_client/_sync.py
@@ -11,7 +11,14 @@
import httpx
-from re3data._client.base import BaseClient, Endpoint, ResourceType, ReturnType, is_valid_return_type
+from re3data._client.base import (
+ BaseClient,
+ Endpoint,
+ ResourceType,
+ ReturnType,
+ _build_query_params,
+ is_valid_return_type,
+)
from re3data._exceptions import RepositoryNotFoundError
from re3data._response import Response, _build_response, _parse_repositories_response, _parse_repository_response
@@ -82,10 +89,14 @@ class RepositoryManager:
def __init__(self, client: Client) -> None:
self._client = client
- def list(self, return_type: ReturnType = ReturnType.DATACLASS) -> list[RepositorySummary] | Response | str:
+ def list(
+ self, query: str | None = None, return_type: ReturnType = ReturnType.DATACLASS
+ ) -> list[RepositorySummary] | Response | str:
"""List the metadata of all repositories in the re3data API.
Args:
+ query: A query string to filter the results. If provided, only repositories matching the query
+ will be returned.
return_type: The desired return type for the API resource. Defaults to `ReturnType.DATACLASS`.
Returns:
@@ -97,7 +108,8 @@ def list(self, return_type: ReturnType = ReturnType.DATACLASS) -> list[Repositor
httpx.HTTPStatusError: If the server returned an error status code >= 500.
"""
is_valid_return_type(return_type)
- response = self._client._request(Endpoint.REPOSITORY_LIST.value)
+ query_params = _build_query_params(query)
+ response = self._client._request(Endpoint.REPOSITORY_LIST.value, query_params)
return _dispatch_return_type(response, ResourceType.REPOSITORY_LIST, return_type)
def get(self, repository_id: str, return_type: ReturnType = ReturnType.DATACLASS) -> Repository | Response | str:
@@ -135,6 +147,9 @@ class Client(BaseClient):
>>> response
[RepositorySummary(id='r3d100010468', doi='https://doi.org/10.17616/R3QP53', name='Zenodo', link=Link(href='https://www.re3data.org/api/beta/repository/r3d100010468', rel='self'))]
... (remaining repositories truncated)
+ >>> response = client.repositories.list(query="biosharing")
+ >>> response
+ [RepositorySummary(id='r3d100010142', doi='https://doi.org/10.17616/R3WS3X', name='FAIRsharing', link=Link(href='https://www.re3data.org/api/beta/repository/r3d100010142', rel='self'))]
"""
_client: httpx.Client
@@ -144,11 +159,13 @@ def __init__(self) -> None:
self._client.event_hooks["response"] = [log_response]
self._repository_manager: RepositoryManager = RepositoryManager(self)
- def _request(self, path: str) -> Response:
+ def _request(self, path: str, query_params: dict[str, str] | None = None) -> Response:
"""Send a HTTP GET request to the specified API endpoint.
Args:
path: The path to send the request to.
+ query_params: Optional URL query parameters to be sent with the HTTP GET request. This dictionary
+ contains key-value pairs that will be added as query parameters to the API endpoint specified by path.
Returns:
The response object from the HTTP request.
@@ -157,7 +174,7 @@ def _request(self, path: str) -> Response:
httpx.HTTPStatusError: If the server returned an error status code >= 500.
RepositoryNotFoundError: If the `repository_id` is not found.
"""
- http_response = self._client.get(path)
+ http_response = self._client.get(path, params=query_params)
if http_response.is_server_error:
http_response.raise_for_status()
return _build_response(http_response)
diff --git a/src/re3data/_client/base.py b/src/re3data/_client/base.py
index c4ffc58..eaec962 100644
--- a/src/re3data/_client/base.py
+++ b/src/re3data/_client/base.py
@@ -54,6 +54,22 @@ def is_valid_return_type(return_type: Any) -> None:
raise ValueError(f"Invalid value for `return_type`: {return_type} is not one of {allowed_types}.")
+def _build_query_params(query: str | None = None) -> dict[str, str]:
+ """Build query parameters based on the input query string.
+
+ Args:
+ query: The input query string. Defaults to None.
+
+ Returns:
+ A dictionary containing the query parameter(s). If no query is provided,
+ the function returns an empty dictionary.
+ """
+ query_params = {}
+ if query:
+ query_params["query"] = query
+ return query_params
+
+
class BaseClient:
"""An abstract base class for clients that interact with the re3data API."""
diff --git a/tests/conftest.py b/tests/conftest.py
index 4c0309f..1fe55a9 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -69,6 +69,31 @@ def mock_repository_list_route(respx_mock: MockRouter, repository_list_xml: str)
)
+@pytest.fixture()
+def mock_repository_list_query_route(respx_mock: MockRouter) -> Route:
+ query_result_xml = """
+
+
+ r3d100010142
+ https://doi.org/10.17616/R3WS3X
+ FAIRsharing
+
+
+
+ """
+ return respx_mock.get("https://www.re3data.org/api/beta/repositories?query=biosharing").mock(
+ return_value=httpx.Response(httpx.codes.OK, text=query_result_xml)
+ )
+
+
+@pytest.fixture()
+def mock_repository_list_query_empty_list_route(respx_mock: MockRouter) -> Route:
+ query_result_xml = '
'
+ return respx_mock.get("https://www.re3data.org/api/beta/repositories?query=XXX").mock(
+ return_value=httpx.Response(httpx.codes.OK, text=query_result_xml)
+ )
+
+
REPOSITORY_GET_XML: str = """
diff --git a/tests/integration/test_async_client.py b/tests/integration/test_async_client.py
index 187ed36..f7d6055 100644
--- a/tests/integration/test_async_client.py
+++ b/tests/integration/test_async_client.py
@@ -55,6 +55,24 @@ async def test_client_list_repositories_response(async_client: AsyncClient, mock
assert response.status_code == httpx.codes.OK
+async def test_client_list_repositories_query_string(
+ async_client: AsyncClient, mock_repository_list_query_route: Route
+) -> None:
+ response = await async_client.repositories.list(query="biosharing")
+ assert isinstance(response, list)
+ repository = response[0]
+ assert isinstance(repository, RepositorySummary)
+ assert repository.id == "r3d100010142"
+
+
+async def test_client_list_repositories_query_string_returns_empty_list(
+ async_client: AsyncClient, mock_repository_list_query_empty_list_route: Route
+) -> None:
+ response = await async_client.repositories.list(query="XXX")
+ assert isinstance(response, list)
+ assert response == []
+
+
async def test_client_get_single_repository_default_return_type(
async_client: AsyncClient, mock_repository_get_route: Route, zenodo_id: str
) -> None:
diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py
index 8f67303..f8eaaa5 100644
--- a/tests/integration/test_cli.py
+++ b/tests/integration/test_cli.py
@@ -102,6 +102,18 @@ def test_repository_list_invalid_return_type(mock_repository_list_route: Route)
assert "Invalid value for '--return-type': 'json'" in result.output
+def test_repository_list_query(mock_repository_list_query_route: Route) -> None:
+ result = runner.invoke(app, ["repository", "list", "--query", "biosharing"])
+ assert result.exit_code == 0
+ assert "id='r3d100010142'" in result.output
+ assert "doi='https://doi.org/10.17616/R3WS3X'" in result.output
+
+
+def test_repository_list_query_returns_empty_list(mock_repository_list_query_empty_list_route: Route) -> None:
+ result = runner.invoke(app, ["repository", "list", "--query", "XXX"])
+ assert result.exit_code == 0
+
+
def test_repository_get_without_repository_id(mock_repository_list_route: Route) -> None:
result = runner.invoke(app, ["repository", "get"])
assert result.exit_code == 2
diff --git a/tests/integration/test_client.py b/tests/integration/test_client.py
index d1d0bd9..13c97da 100644
--- a/tests/integration/test_client.py
+++ b/tests/integration/test_client.py
@@ -55,6 +55,22 @@ def test_client_list_repositories_response(client: Client, mock_repository_list_
assert response.status_code == httpx.codes.OK
+def test_client_list_repositories_query_string(client: Client, mock_repository_list_query_route: Route) -> None:
+ response = client.repositories.list(query="biosharing")
+ assert isinstance(response, list)
+ repository = response[0]
+ assert isinstance(repository, RepositorySummary)
+ assert repository.id == "r3d100010142"
+
+
+def test_client_list_repositories_query_string_returns_empty_list(
+ client: Client, mock_repository_list_query_empty_list_route: Route
+) -> None:
+ response = client.repositories.list(query="XXX")
+ assert isinstance(response, list)
+ assert response == []
+
+
def test_client_get_single_repository_default_return_type(
client: Client, mock_repository_get_route: Route, zenodo_id: str
) -> None: