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 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)