datacite 1.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- datacite/__init__.py +14 -0
- datacite/client.py +183 -0
- datacite/errors.py +99 -0
- datacite/jsonutils.py +23 -0
- datacite/request.py +109 -0
- datacite/rest_client.py +329 -0
- datacite/schema40.py +392 -0
- datacite/schema41.py +417 -0
- datacite/schema42.py +428 -0
- datacite/schema43.py +441 -0
- datacite/schema45.py +551 -0
- datacite/schemas/datacite-v4.0.json +607 -0
- datacite/schemas/datacite-v4.1.json +700 -0
- datacite/schemas/datacite-v4.2.json +480 -0
- datacite/schemas/datacite-v4.3.json +495 -0
- datacite/schemas/datacite-v4.5.json +628 -0
- datacite/schemas/ietf-bcp-47.json +10 -0
- datacite/schemas/iso-8601.json +31 -0
- datacite/xmlutils.py +80 -0
- datacite-1.4.1.dist-info/METADATA +103 -0
- datacite-1.4.1.dist-info/RECORD +23 -0
- datacite-1.4.1.dist-info/WHEEL +4 -0
- datacite-1.4.1.dist-info/licenses/LICENSE +37 -0
datacite/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2015, 2016 CERN.
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 Graz University of Technology.
|
|
3
|
+
# SPDX-FileCopyrightText: 2026 TU Wien.
|
|
4
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
"""Python API wrapper for the DataCite API."""
|
|
8
|
+
|
|
9
|
+
from .client import DataCiteMDSClient
|
|
10
|
+
from .rest_client import DataCiteRESTClient
|
|
11
|
+
|
|
12
|
+
__version__ = "1.4.1"
|
|
13
|
+
|
|
14
|
+
__all__ = ("DataCiteMDSClient", "DataCiteRESTClient", "__version__")
|
datacite/client.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2015 CERN.
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
"""Python API client wrapper for the DataCite Metadata Store API.
|
|
5
|
+
|
|
6
|
+
API documentation is available at
|
|
7
|
+
https://support.datacite.org/docs/mds-api-guide.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from .errors import DataCiteError
|
|
13
|
+
from .request import DataCiteRequest
|
|
14
|
+
|
|
15
|
+
HTTP_OK = requests.codes["ok"]
|
|
16
|
+
HTTP_CREATED = requests.codes["created"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DataCiteMDSClient(object):
|
|
20
|
+
"""DataCite MDS API client wrapper.
|
|
21
|
+
|
|
22
|
+
Warning: The DataCite MDS API is being maintained but is no longer actively
|
|
23
|
+
developed.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self, username, password, prefix, test_mode=False, url=None, timeout=None
|
|
28
|
+
):
|
|
29
|
+
"""Initialize the API client wrapper.
|
|
30
|
+
|
|
31
|
+
:param username: DataCite username.
|
|
32
|
+
:param password: DataCite password.
|
|
33
|
+
:param prefix: DOI prefix (or CFG_DATACITE_DOI_PREFIX).
|
|
34
|
+
:param test_mode: use test URL when True
|
|
35
|
+
:param url: DataCite API base URL.
|
|
36
|
+
:param timeout: Connect and read timeout in seconds. Specify a tuple
|
|
37
|
+
(connect, read) to specify each timeout individually.
|
|
38
|
+
"""
|
|
39
|
+
self.username = username
|
|
40
|
+
self.password = password
|
|
41
|
+
self.prefix = prefix
|
|
42
|
+
|
|
43
|
+
if test_mode:
|
|
44
|
+
self.api_url = "https://mds.test.datacite.org/"
|
|
45
|
+
else:
|
|
46
|
+
self.api_url = url or "https://mds.datacite.org/"
|
|
47
|
+
|
|
48
|
+
if not self.api_url.endswith("/"):
|
|
49
|
+
self.api_url += "/"
|
|
50
|
+
|
|
51
|
+
self.timeout = timeout
|
|
52
|
+
|
|
53
|
+
def __repr__(self):
|
|
54
|
+
"""Create string representation of object."""
|
|
55
|
+
return "<DataCiteMDSClient: {0}>".format(self.username)
|
|
56
|
+
|
|
57
|
+
def _create_request(self):
|
|
58
|
+
"""Create a new Request object."""
|
|
59
|
+
return DataCiteRequest(
|
|
60
|
+
base_url=self.api_url,
|
|
61
|
+
username=self.username,
|
|
62
|
+
password=self.password,
|
|
63
|
+
timeout=self.timeout,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def doi_get(self, doi):
|
|
67
|
+
"""Get the URL where the resource pointed by the DOI is located.
|
|
68
|
+
|
|
69
|
+
:param doi: DOI name of the resource.
|
|
70
|
+
"""
|
|
71
|
+
request = self._create_request()
|
|
72
|
+
resp = request.get("doi/" + doi)
|
|
73
|
+
if resp.status_code == HTTP_OK:
|
|
74
|
+
return resp.text
|
|
75
|
+
else:
|
|
76
|
+
raise DataCiteError.factory(resp.status_code, resp.text)
|
|
77
|
+
|
|
78
|
+
def doi_post(self, new_doi, location):
|
|
79
|
+
"""Mint new DOI.
|
|
80
|
+
|
|
81
|
+
:param new_doi: DOI name for the new resource.
|
|
82
|
+
:param location: URL where the resource is located.
|
|
83
|
+
:return: "CREATED" or "HANDLE_ALREADY_EXISTS".
|
|
84
|
+
"""
|
|
85
|
+
headers = {"Content-Type": "text/plain;charset=UTF-8"}
|
|
86
|
+
# Use \r\n for HTTP client data.
|
|
87
|
+
body = "\r\n".join(["doi=%s" % new_doi, "url=%s" % location])
|
|
88
|
+
|
|
89
|
+
request = self._create_request()
|
|
90
|
+
resp = request.post("doi", body=body, headers=headers)
|
|
91
|
+
|
|
92
|
+
if resp.status_code == HTTP_CREATED:
|
|
93
|
+
return resp.text
|
|
94
|
+
else:
|
|
95
|
+
raise DataCiteError.factory(resp.status_code, resp.text)
|
|
96
|
+
|
|
97
|
+
def metadata_get(self, doi):
|
|
98
|
+
"""Get the XML metadata associated to a DOI name.
|
|
99
|
+
|
|
100
|
+
:param doi: DOI name of the resource.
|
|
101
|
+
"""
|
|
102
|
+
headers = {"Accept": "application/xml", "Accept-Encoding": "UTF-8"}
|
|
103
|
+
|
|
104
|
+
request = self._create_request()
|
|
105
|
+
resp = request.get("metadata/" + doi, headers=headers)
|
|
106
|
+
|
|
107
|
+
if resp.status_code == HTTP_OK:
|
|
108
|
+
return resp.text
|
|
109
|
+
else:
|
|
110
|
+
raise DataCiteError.factory(resp.status_code, resp.text)
|
|
111
|
+
|
|
112
|
+
def metadata_post(self, metadata):
|
|
113
|
+
"""Set new metadata for an existing DOI.
|
|
114
|
+
|
|
115
|
+
Metadata should follow the DataCite Metadata Schema:
|
|
116
|
+
http://schema.datacite.org/
|
|
117
|
+
|
|
118
|
+
:param metadata: XML format of the metadata.
|
|
119
|
+
:return: "CREATED" or "HANDLE_ALREADY_EXISTS"
|
|
120
|
+
"""
|
|
121
|
+
headers = {
|
|
122
|
+
"Content-Type": "application/xml;charset=UTF-8",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
request = self._create_request()
|
|
126
|
+
resp = request.post("metadata", body=metadata, headers=headers)
|
|
127
|
+
|
|
128
|
+
if resp.status_code == HTTP_CREATED:
|
|
129
|
+
return resp.text
|
|
130
|
+
else:
|
|
131
|
+
raise DataCiteError.factory(resp.status_code, resp.text)
|
|
132
|
+
|
|
133
|
+
def metadata_delete(self, doi):
|
|
134
|
+
"""Mark as 'inactive' the metadata set of a DOI resource.
|
|
135
|
+
|
|
136
|
+
:param doi: DOI name of the resource.
|
|
137
|
+
:return: "OK"
|
|
138
|
+
"""
|
|
139
|
+
request = self._create_request()
|
|
140
|
+
resp = request.delete("metadata/" + doi)
|
|
141
|
+
|
|
142
|
+
if resp.status_code == HTTP_OK:
|
|
143
|
+
return resp.text
|
|
144
|
+
else:
|
|
145
|
+
raise DataCiteError.factory(resp.status_code, resp.text)
|
|
146
|
+
|
|
147
|
+
def media_get(self, doi):
|
|
148
|
+
"""Get list of pairs of media type and URLs associated with a DOI.
|
|
149
|
+
|
|
150
|
+
:param doi: DOI name of the resource.
|
|
151
|
+
"""
|
|
152
|
+
request = self._create_request()
|
|
153
|
+
resp = request.get("media/" + doi)
|
|
154
|
+
|
|
155
|
+
if resp.status_code == HTTP_OK:
|
|
156
|
+
values = {}
|
|
157
|
+
for line in resp.text.splitlines():
|
|
158
|
+
mimetype, url = line.split("=", 1)
|
|
159
|
+
values[mimetype] = url
|
|
160
|
+
return values
|
|
161
|
+
else:
|
|
162
|
+
raise DataCiteError.factory(resp.status_code, resp.text)
|
|
163
|
+
|
|
164
|
+
def media_post(self, doi, media):
|
|
165
|
+
"""Add/update media type/urls pairs to a DOI.
|
|
166
|
+
|
|
167
|
+
Standard domain restrictions check will be performed.
|
|
168
|
+
|
|
169
|
+
:param media: Dictionary of (mime-type, URL) key/value pairs.
|
|
170
|
+
:return: "OK"
|
|
171
|
+
"""
|
|
172
|
+
headers = {"Content-Type": "text/plain;charset=UTF-8"}
|
|
173
|
+
|
|
174
|
+
# Use \r\n for HTTP client data.
|
|
175
|
+
body = "\r\n".join(["%s=%s" % (k, v) for k, v in media.items()])
|
|
176
|
+
|
|
177
|
+
request = self._create_request()
|
|
178
|
+
resp = request.post("media/" + doi, body=body, headers=headers)
|
|
179
|
+
|
|
180
|
+
if resp.status_code == HTTP_OK:
|
|
181
|
+
return resp.text
|
|
182
|
+
else:
|
|
183
|
+
raise DataCiteError.factory(resp.status_code, resp.text)
|
datacite/errors.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2015 CERN.
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
"""Errors for the DataCite API.
|
|
5
|
+
|
|
6
|
+
MDS error responses will be converted into an exception from this module.
|
|
7
|
+
Connection issues raises :py:exc:`datacite.errors.HttpError` while DataCite
|
|
8
|
+
MDS error responses raises a subclass of
|
|
9
|
+
:py:exc:`datacite.errors.DataCiteError`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HttpError(Exception):
|
|
14
|
+
"""Exception raised when a connection problem happens."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DataCiteError(Exception):
|
|
18
|
+
"""Exception raised when the server returns a known HTTP error code.
|
|
19
|
+
|
|
20
|
+
Known HTTP error codes include:
|
|
21
|
+
|
|
22
|
+
* 204 No Content
|
|
23
|
+
* 400 Bad Request
|
|
24
|
+
* 401 Unauthorized
|
|
25
|
+
* 403 Forbidden
|
|
26
|
+
* 404 Not Found
|
|
27
|
+
* 410 Gone (deleted)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def factory(err_code, *args):
|
|
32
|
+
"""Create exceptions through a Factory based on the HTTP error code."""
|
|
33
|
+
if err_code == 204:
|
|
34
|
+
return DataCiteNoContentError(*args)
|
|
35
|
+
elif err_code == 400:
|
|
36
|
+
return DataCiteBadRequestError(*args)
|
|
37
|
+
elif err_code == 401:
|
|
38
|
+
return DataCiteUnauthorizedError(*args)
|
|
39
|
+
elif err_code == 403:
|
|
40
|
+
return DataCiteForbiddenError(*args)
|
|
41
|
+
elif err_code == 404:
|
|
42
|
+
return DataCiteNotFoundError(*args)
|
|
43
|
+
elif err_code == 410:
|
|
44
|
+
return DataCiteGoneError(*args)
|
|
45
|
+
elif err_code == 412:
|
|
46
|
+
return DataCitePreconditionError(*args)
|
|
47
|
+
else:
|
|
48
|
+
return DataCiteServerError(*args)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DataCiteServerError(DataCiteError):
|
|
52
|
+
"""An internal server error happened on the DataCite end. Try later.
|
|
53
|
+
|
|
54
|
+
Base class for all 5XX-related HTTP error codes.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DataCiteRequestError(DataCiteError):
|
|
59
|
+
"""A DataCite request error. You made an invalid request.
|
|
60
|
+
|
|
61
|
+
Base class for all 4XX-related HTTP error codes as well as 204.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DataCiteNoContentError(DataCiteRequestError):
|
|
66
|
+
"""DOI is known to MDS, but not resolvable.
|
|
67
|
+
|
|
68
|
+
This might be due to handle's latency.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DataCiteBadRequestError(DataCiteRequestError):
|
|
73
|
+
"""Bad request error.
|
|
74
|
+
|
|
75
|
+
Bad requests can include e.g. invalid XML, wrong domain, wrong prefix.
|
|
76
|
+
Request body must be exactly two lines: DOI and URL
|
|
77
|
+
One or more of the specified mime-types or urls are invalid (e.g. non
|
|
78
|
+
supported mimetype, not allowed url domain, etc.)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class DataCiteUnauthorizedError(DataCiteRequestError):
|
|
83
|
+
"""Bad username or password."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DataCiteForbiddenError(DataCiteRequestError):
|
|
87
|
+
"""Login problem, dataset belongs to another party or quota exceeded."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class DataCiteNotFoundError(DataCiteRequestError):
|
|
91
|
+
"""DOI does not exist in the database."""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DataCiteGoneError(DataCiteRequestError):
|
|
95
|
+
"""Requested dataset was marked inactive (using DELETE method)."""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DataCitePreconditionError(DataCiteRequestError):
|
|
99
|
+
"""Metadata must be uploaded first."""
|
datacite/jsonutils.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2016 CERN.
|
|
2
|
+
# SPDX-FileCopyrightText: 2025 Graz University of Technology.
|
|
3
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
4
|
+
|
|
5
|
+
"""JSON utilities."""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from jsonresolver.contrib.jsonschema import RefResolverBase
|
|
10
|
+
from jsonschema.validators import validator_for
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validator_factory(schema_filename):
|
|
14
|
+
"""Provide a JSON schema validator for a given schema file."""
|
|
15
|
+
with open(schema_filename, "r") as fp:
|
|
16
|
+
schema = json.load(fp)
|
|
17
|
+
|
|
18
|
+
validator_cls = validator_for(schema)
|
|
19
|
+
validator_cls.check_schema(schema)
|
|
20
|
+
|
|
21
|
+
return validator_cls(
|
|
22
|
+
schema, resolver=RefResolverBase("file:{}".format(schema_filename), schema)
|
|
23
|
+
)
|
datacite/request.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2015-2018 CERN.
|
|
2
|
+
# SPDX-FileCopyrightText: 2018 Center for Open Science.
|
|
3
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
4
|
+
|
|
5
|
+
"""Module for making requests to the DataCite API."""
|
|
6
|
+
|
|
7
|
+
import ssl
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from requests.auth import HTTPBasicAuth
|
|
11
|
+
from requests.exceptions import RequestException
|
|
12
|
+
|
|
13
|
+
from .errors import HttpError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DataCiteRequest(object):
|
|
17
|
+
"""Helper class for making requests.
|
|
18
|
+
|
|
19
|
+
:param base_url: Base URL for all requests.
|
|
20
|
+
:param username: HTTP Basic Authentication Username
|
|
21
|
+
:param password: HTTP Basic Authentication Password
|
|
22
|
+
:param default_params: A key/value-mapping which will be converted into a
|
|
23
|
+
query string on all requests.
|
|
24
|
+
:param timeout: Connect and read timeout in seconds. Specify a tuple
|
|
25
|
+
(connect, read) to specify each timeout individually.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
base_url=None,
|
|
31
|
+
username=None,
|
|
32
|
+
password=None,
|
|
33
|
+
default_params=None,
|
|
34
|
+
timeout=None,
|
|
35
|
+
):
|
|
36
|
+
"""Initialize request object."""
|
|
37
|
+
self.base_url = base_url
|
|
38
|
+
self.username = username
|
|
39
|
+
self.password = password.encode("utf8")
|
|
40
|
+
self.default_params = default_params or {}
|
|
41
|
+
self.timeout = timeout
|
|
42
|
+
|
|
43
|
+
def request(self, url, method="GET", body=None, params=None, headers=None):
|
|
44
|
+
"""Make a request.
|
|
45
|
+
|
|
46
|
+
If the request was successful (i.e no exceptions), you can find the
|
|
47
|
+
HTTP response code in self.code and the response body in self.value.
|
|
48
|
+
|
|
49
|
+
:param url: Request URL (relative to base_url if set)
|
|
50
|
+
:param method: Request method (GET, POST, DELETE) supported
|
|
51
|
+
:param body: Request body
|
|
52
|
+
:param params: Request parameters
|
|
53
|
+
:param headers: Request headers
|
|
54
|
+
"""
|
|
55
|
+
params = params or {}
|
|
56
|
+
headers = headers or {}
|
|
57
|
+
|
|
58
|
+
self.data = None
|
|
59
|
+
self.code = None
|
|
60
|
+
|
|
61
|
+
if self.default_params:
|
|
62
|
+
params.update(self.default_params)
|
|
63
|
+
|
|
64
|
+
if self.base_url:
|
|
65
|
+
url = self.base_url + url
|
|
66
|
+
|
|
67
|
+
if body and isinstance(body, str):
|
|
68
|
+
body = body.encode("utf-8")
|
|
69
|
+
|
|
70
|
+
request_func = getattr(requests, method.lower())
|
|
71
|
+
kwargs = dict(
|
|
72
|
+
auth=HTTPBasicAuth(self.username, self.password),
|
|
73
|
+
params=params,
|
|
74
|
+
headers=headers,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if method == "POST":
|
|
78
|
+
kwargs["data"] = body
|
|
79
|
+
if method == "PUT":
|
|
80
|
+
kwargs["data"] = body
|
|
81
|
+
if self.timeout is not None:
|
|
82
|
+
kwargs["timeout"] = self.timeout
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
return request_func(url, **kwargs)
|
|
86
|
+
except RequestException as e:
|
|
87
|
+
raise HttpError(e)
|
|
88
|
+
except ssl.SSLError as e:
|
|
89
|
+
raise HttpError(e)
|
|
90
|
+
|
|
91
|
+
def get(self, url, params=None, headers=None):
|
|
92
|
+
"""Make a GET request."""
|
|
93
|
+
return self.request(url, params=params, headers=headers)
|
|
94
|
+
|
|
95
|
+
def post(self, url, body=None, params=None, headers=None):
|
|
96
|
+
"""Make a POST request."""
|
|
97
|
+
return self.request(
|
|
98
|
+
url, method="POST", body=body, params=params, headers=headers
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def put(self, url, body=None, params=None, headers=None):
|
|
102
|
+
"""Make a PUT request."""
|
|
103
|
+
return self.request(
|
|
104
|
+
url, method="PUT", body=body, params=params, headers=headers
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def delete(self, url, params=None, headers=None):
|
|
108
|
+
"""Make a DELETE request."""
|
|
109
|
+
return self.request(url, method="DELETE", params=params, headers=headers)
|