Skip to content

Commit 62300bc

Browse files
authored
feat: set up client (#64)
Fixes #2, fixes #15, fixes #16
1 parent 141208d commit 62300bc

File tree

10 files changed

+2047
-1
lines changed

10 files changed

+2047
-1
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ ci:
1313
# Exclude changelog: auto-generated by python-semantic-release
1414
exclude: |
1515
(?x)^(
16-
/cassettes/|
16+
tests/cassettes/repository.yaml|
17+
tests/cassettes/repositories.yaml|
1718
CHANGELOG.md
1819
)$
1920

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,43 @@
1515
of Research Data Repositories) [REST API](https://www.re3data.org/api/doc), allowing you to easily retrieve and process
1616
metadata about research data repositories in a convenient and Pythonic way.
1717

18+
```pycon
19+
>>> import re3data
20+
>>> response = re3data.repositories.list()
21+
>>> print(response)
22+
<?xml version="1.0" encoding="UTF-8"?>
23+
<list>
24+
<repository>
25+
<id>r3d100010468</id>
26+
<doi>https://doi.org/10.17616/R3QP53</doi>
27+
<name>Zenodo</name>
28+
<link href="https://www.re3data.org/api/beta/repository/r3d100010468" rel="self" />
29+
</repository>
30+
... (remaining repositories truncated)
31+
```
32+
33+
```pycon
34+
>>> response = re3data.repositories.get("r3d100010468")
35+
>>> print(response)
36+
<?xml version="1.0" encoding="utf-8"?>
37+
<!--re3data.org Schema for the Description of Research Data Repositories. Version 2.2, December 2014. doi:10.2312/re3.006-->
38+
<r3d:re3data xmlns:r3d="http://www.re3data.org/schema/2-2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.re3data.org/schema/2-2 http://schema.re3data.org/2-2/re3dataV2-2.xsd">
39+
<r3d:repository>
40+
<r3d:re3data.orgIdentifier>r3d100010468</r3d:re3data.orgIdentifier>
41+
<r3d:repositoryName language="eng">Zenodo</r3d:repositoryName>
42+
<r3d:repositoryURL>https://zenodo.org/</r3d:repositoryURL>
43+
... (remaining fields truncated)
44+
```
45+
46+
## Features
47+
48+
- Pythonic API interactions: Interact with the re3data API in a Pythonic way, without having to worry about low-level
49+
HTTP requests or XML parsing.
50+
- Repository metadata retrieval: Easily fetch and process metadata about research data repositories using
51+
`re3data.repositories.list()`.
52+
- Repository details retrieval: Get detailed information about a specific repository using
53+
`re3data.repositories.get(repository_id)`.
54+
1855
## Requirements
1956

2057
[Python](https://www.python.org/downloads/) >= 3.10

docs/src/api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
# API Reference
2+
3+
## `Client`
4+
5+
::: re3data.Client

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ omit = [
223223
[tool.coverage.report]
224224
exclude_also = [
225225
"if TYPE_CHECKING:",
226+
"@abstractmethod",
226227
]
227228
fail_under = 90
228229
show_missing = true

src/re3data/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
"""python-re3data."""
66

77
from re3data.__about__ import __version__
8+
from re3data._client import Client
89

910
__all__ = [
1011
"__version__",
12+
"Client",
1113
]
14+
15+
_client = Client()
16+
repositories = _client.repositories

src/re3data/_client.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# SPDX-FileCopyrightText: 2024 Heinz-Alexander Fütterer
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""The _client module provides a client for interacting with the re3data API."""
6+
7+
from __future__ import annotations
8+
9+
import logging
10+
from abc import ABC, abstractmethod
11+
12+
import httpx
13+
14+
from re3data import __version__
15+
16+
logger = logging.getLogger(__name__)
17+
18+
BASE_URL: str = "https://www.re3data.org/api/beta/"
19+
DEFAULT_HEADERS: dict[str, str] = {
20+
"Accept": "text/xml; charset=utf-8",
21+
"User-Agent": f"python-re3data/{__version__}",
22+
}
23+
DEFAULT_TIMEOUT = httpx.Timeout(timeout=10.0) # timeout in seconds
24+
25+
26+
def log_response(response: httpx.Response) -> None:
27+
"""Log the details of an HTTP response.
28+
29+
This function logs the HTTP method, URL, and status code of the response for debugging purposes.
30+
It uses the 'debug' logging level to provide detailed diagnostic information.
31+
32+
Args:
33+
response: The response object received from an HTTP request.
34+
35+
Returns:
36+
None
37+
"""
38+
logger.debug(
39+
"[http] Response: %s %s - Status %s", response.request.method, response.request.url, response.status_code
40+
)
41+
42+
43+
class RepositoryManager:
44+
"""A manager for interacting with repositories in the re3data API.
45+
46+
Attributes:
47+
_client: The client used to make requests.
48+
"""
49+
50+
def __init__(self, client: Client) -> None:
51+
self._client = client
52+
53+
def list(self, return_type: str = "xml") -> str | httpx.Response:
54+
"""List the metadata of all repositories in the re3data API.
55+
56+
Args:
57+
return_type: The type of response to expect. Defaults to "xml".
58+
59+
Returns:
60+
A string representation of the response (if `return_type` is "xml") or the full response object.
61+
"""
62+
return self._client._request("repositories", return_type)
63+
64+
def get(self, repository_id: str, return_type: str = "xml") -> str | httpx.Response:
65+
"""Get the metadata of a specific repository.
66+
67+
Args:
68+
repository_id: The identifier of the repository to retrieve.
69+
return_type: The type of response to expect. Defaults to "xml".
70+
71+
Returns:
72+
A string representation of the response (if `return_type` is "xml") or the full response object.
73+
"""
74+
return self._client._request(f"repository/{repository_id}", return_type)
75+
76+
77+
class BaseClient(ABC):
78+
"""An abstract base class for clients that interact with the re3data API."""
79+
80+
def __init__(
81+
self,
82+
client: type[httpx.Client] | type[httpx.AsyncClient],
83+
) -> None:
84+
self._client = client(
85+
base_url=BASE_URL,
86+
headers=DEFAULT_HEADERS,
87+
timeout=DEFAULT_TIMEOUT,
88+
follow_redirects=True,
89+
event_hooks={"response": [log_response]},
90+
)
91+
92+
@abstractmethod
93+
def _request(self, endpoint: str, return_type: str) -> str | httpx.Response:
94+
pass
95+
96+
97+
class Client(BaseClient):
98+
"""A client that interacts with the re3data API.
99+
100+
Attributes:
101+
_client: The underlying HTTP client.
102+
_repository_manager: The repository manager to retrieve metadata from the repositories endpoints.
103+
104+
Examples:
105+
>>> client = Client():
106+
>>> response = re3data.repositories.list()
107+
>>> print(response)
108+
<?xml version="1.0" encoding="UTF-8"?>
109+
<list>
110+
<repository>
111+
<id>r3d100010468</id>
112+
<doi>https://doi.org/10.17616/R3QP53</doi>
113+
<name>Zenodo</name>
114+
<link href="https://www.re3data.org/api/beta/repository/r3d100010468" rel="self" />
115+
</repository>
116+
... (remaining repositories truncated)
117+
"""
118+
119+
_client: httpx.Client
120+
121+
def __init__(self) -> None:
122+
super().__init__(httpx.Client)
123+
self._repository_manager: RepositoryManager = RepositoryManager(self)
124+
125+
def _request(self, endpoint: str, return_type: str) -> str | httpx.Response:
126+
"""Send a HTTP GET request to the specified endpoint.
127+
128+
Args:
129+
endpoint: The endpoint to send the request to.
130+
return_type: The type of response to expect.
131+
132+
Returns:
133+
A string representation of the response (if `return_type` is "xml") or the full response object.
134+
135+
Raises:
136+
httpx.RequestError: If the request fails or times out.
137+
ValueError: If an invalid `return_type` is provided.
138+
"""
139+
response = self._client.get(endpoint)
140+
response.raise_for_status()
141+
match return_type:
142+
case "xml":
143+
return response.text
144+
case "response":
145+
return response
146+
case _:
147+
raise ValueError(f"Invalid `return_type`: {return_type}. Expected one of: `xml`, `response`.")
148+
149+
@property
150+
def repositories(self) -> RepositoryManager:
151+
"""Get the repository manager for this client.
152+
153+
Returns:
154+
The repository manager.
155+
"""
156+
return self._repository_manager

0 commit comments

Comments
 (0)