ibridgesdvn 1.0.0__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.
@@ -0,0 +1,38 @@
1
+ """Create links to python classes."""
2
+
3
+ from ibridgescontrib.ibridgesdvn.dvn_dataset_commands import (
4
+ CliDvnAddFile,
5
+ CliDvnCleanUp,
6
+ CliDvnCreateDataset,
7
+ CliDvnPush,
8
+ CliDvnRmFile,
9
+ CliDvnStatus,
10
+ )
11
+ from ibridgescontrib.ibridgesdvn.dvn_init import CliDvnInit, CliDvnSwitch
12
+ from ibridgescontrib.ibridgesdvn.dvn_setup import CliDvnAlias
13
+
14
+ SETUP_COMMANDS = [CliDvnAlias]
15
+ INIT_COMMANDS = [CliDvnInit]
16
+ SWITCH_COMMANDS = [CliDvnSwitch]
17
+ PUSH_COMMANDS = [CliDvnPush]
18
+ STATUS_COMMANDS = [CliDvnStatus]
19
+ CLEANUP_COMMANDS = [CliDvnCleanUp]
20
+ RM_FILE_COMMANDS = [CliDvnRmFile]
21
+ ADD_FILE_COMMANDS = [CliDvnAddFile]
22
+ CREATE_DS_COMMANDS = [CliDvnCreateDataset]
23
+
24
+ __all__ = ["get_dataverse_tab"]
25
+
26
+ # pylint: disable=import-outside-toplevel
27
+ def get_dataverse_tab():
28
+ """Import for GUI."""
29
+ try:
30
+ from ibridgescontrib.ibridgesdvn.gui.gui_dataverse import (
31
+ DataverseTab,
32
+ )
33
+ except ImportError as e:
34
+ raise ImportError(
35
+ "GUI dependencies missing. Install with:\n"
36
+ " pip install ibridgescontrib[gui]"
37
+ ) from e
38
+ return DataverseTab
@@ -0,0 +1,274 @@
1
+ """Dataverse functionality for creating a draft."""
2
+
3
+ from pathlib import Path
4
+
5
+ from httpx import Client, Request
6
+ from httpx._exceptions import HTTPError
7
+ from pyDataverse.api import NativeApi
8
+ from pyDataverse.auth import BearerTokenAuth
9
+ from pyDataverse.exceptions import ApiAuthorizationError
10
+ from pyDataverse.models import Datafile, Dataset
11
+ from pyDataverse.utils import read_file
12
+
13
+
14
+ class Dataverse:
15
+ """A utility class to interact with a Dataverse instance using the provided URL and API token.
16
+
17
+ This class supports authentication, dataset management, and metadata retrieval.
18
+ """
19
+
20
+ def __init__(self, url: str, token: str):
21
+ """Initialise the DataverseOperations instance.
22
+
23
+ Args:
24
+ ----
25
+ url (str): The base URL of the Dataverse instance.
26
+ token (str): The API token used for authentication.
27
+
28
+ Raises:
29
+ ------
30
+ ValueError: If the API token is not provided.
31
+ ApiAuthorizationError: If the token does not successfully authenticate.
32
+
33
+ """
34
+ self.dvn_url = url
35
+ self.dvn_token = token
36
+
37
+ if self.dvn_token is None:
38
+ raise ValueError("Dataverse API token cannot be empty.")
39
+
40
+ if self.user_authenticated():
41
+ self.api = NativeApi(self.dvn_url, self.dvn_token)
42
+ else:
43
+ raise ApiAuthorizationError("Dataverse and API token do not match.")
44
+
45
+ def user_authenticated(self) -> True:
46
+ """Verify if the user token is valid for the Dataverse instance.
47
+
48
+ Returns:
49
+ -------
50
+ bool: True if the token is valid and authentication succeeds.
51
+
52
+ Note:
53
+ ----
54
+ This method performs a basic request with the Bearer token to check authorization.
55
+
56
+ """
57
+ auth = BearerTokenAuth(self.dvn_token)
58
+ request = Request("GET", self.dvn_url)
59
+ modified_request = next(auth.auth_flow(request))
60
+ return modified_request.headers["authorization"] == "Bearer " + self.dvn_token
61
+
62
+ def get_dataverse_info(self, dataverse):
63
+ """Retrieve the children (datasets and sub-dataverses) of a specified Dataverse.
64
+
65
+ Args:
66
+ ----
67
+ dataverse (str): The alias or identifier of the Dataverse.
68
+
69
+ Returns:
70
+ -------
71
+ dict: The response containing child items of the Dataverse.
72
+
73
+ """
74
+ return self.api.get_children(dataverse)
75
+
76
+ def dataverse_exists(self, dataverse: str):
77
+ """Check whether a specified Dataverse exists.
78
+
79
+ Args:
80
+ ----
81
+ dataverse (str): The alias or identifier of the Dataverse.
82
+
83
+ Returns:
84
+ -------
85
+ bool: True if the Dataverse exists; False otherwise.
86
+
87
+ """
88
+ answer = self.api.get_dataverse(dataverse)
89
+ return answer.is_success
90
+
91
+ def list_dataverse_content(self, dataverse: str) -> dict:
92
+ """Retrieve the list of datasets and sub-dataverses contained within a specified Dataverse.
93
+
94
+ Args:
95
+ ----
96
+ dataverse (str): The alias or identifier of the Dataverse to query.
97
+
98
+ Returns:
99
+ -------
100
+ dict: A dictionary representing the JSON response from the Dataverse API,
101
+ typically including datasets and nested dataverses.
102
+
103
+ Raises:
104
+ ------
105
+ HTTPError: If the API response status code is not 200 (i.e., request failed).
106
+
107
+ """
108
+ url = f"https://demo.dataverse.nl/api/dataverses/{dataverse}/contents"
109
+ headers = {"X-Dataverse-key": self.dvn_token}
110
+
111
+ request = Request("GET", url, headers=headers)
112
+ with Client() as client:
113
+ response = client.send(request)
114
+
115
+ if response.status_code in range(200, 300):
116
+ return response.json()
117
+ raise HTTPError(f"{response.status_code}, {response.reason_phrase}")
118
+
119
+ def get_dataset_info(self, data_id: str):
120
+ """Retrieve metadata and information for a dataset using its DOI identifier.
121
+
122
+ Args:
123
+ ----
124
+ data_id (str): The dataset identifier (excluding the 'doi:' prefix).
125
+
126
+ Returns:
127
+ -------
128
+ dict: The JSON response containing dataset metadata and details.
129
+
130
+ Raises:
131
+ ------
132
+ HTTPError: If the API response status code is not 200 (successful).
133
+
134
+ """
135
+ response = self.api.get_dataset(f"doi:{data_id}")
136
+ if response.status_code in range(200, 300):
137
+ return response.json()
138
+ raise HTTPError(f"{response.status_code}, {response.reason_phrase}")
139
+
140
+ def dataset_exists(self, data_id):
141
+ """Look up if data set can be found in Dataverse."""
142
+ response = self.api.get_dataset(f"doi:{data_id}")
143
+ return response.status_code in range(200, 300)
144
+
145
+ def create_dataset_with_json(self, dataverse: str, metadata: Path, verbose: bool = False):
146
+ """Create a new dataset from a json metadata file.
147
+
148
+ Parameters
149
+ ----------
150
+ dataverse:
151
+ The name of the Dataverse Collection where the dataset will be created.
152
+ metadata:
153
+ The path to a valid Dataverse Dataset metadata file.
154
+ verbose:
155
+ Print summary if True.
156
+
157
+ """
158
+ if dataverse is None:
159
+ raise ValueError("Dataverse name must not be empty.")
160
+
161
+ ds = Dataset()
162
+ ds.from_json(read_file(str(metadata)))
163
+
164
+ if verbose:
165
+ print("Dataset metadata ok:", ds.validate_json())
166
+ print(ds.get())
167
+
168
+ if not ds.validate_json():
169
+ raise ValueError("Something is wrong with the dataset's metadata.")
170
+
171
+ response = self.api.create_dataset(dataverse, ds.json())
172
+
173
+ if response.status_code not in range(200, 300):
174
+ raise HTTPError(f"{response.status_code}, {response.reason_phrase}")
175
+
176
+ return response
177
+
178
+ def create_dataset(self, dataverse: str, metadata: str): # pylint: disable=R0913, R0917
179
+ """Create a new dataset in a specified Dataverse repository.
180
+
181
+ Parameters
182
+ ----------
183
+ dataverse:
184
+ The name of the Dataverse Collection where the dataset will be created.
185
+ metadata:
186
+ A minimal metadata json string.
187
+
188
+ Raises
189
+ ------
190
+ ValueError: If any of the required parameters are missing or empty
191
+ (e.g., dataverse, title, subject, authors, contacts).
192
+ HTTPError: If the response from the API is not successful
193
+ (i.e., status code is not 200).
194
+
195
+ Returns
196
+ -------
197
+ None: The function creates the dataset but does not return any value.
198
+
199
+ """
200
+ if dataverse is None:
201
+ raise ValueError("Dataverse name must not be empty.")
202
+ if metadata is None:
203
+ raise ValueError("Provide a dictionary conatining the metadata..")
204
+
205
+ ds = Dataset()
206
+ # ds.set(metadata)
207
+ ds.from_json(metadata)
208
+ if not ds.validate_json():
209
+ raise ValueError("Something is wrong with the dataset's metadata.")
210
+
211
+ response = self.api.create_dataset(dataverse, ds.json())
212
+
213
+ if response.status_code not in range(200, 300):
214
+ raise HTTPError(f"{response.status_code}, {response.reason_phrase}")
215
+
216
+ return response
217
+
218
+ def add_datafile_to_dataset(self, dataset_id: str, file_path: Path, verbose: bool = True):
219
+ """Upload a data file to a specific dataset.
220
+
221
+ Parameters
222
+ ----------
223
+ dataset_id:
224
+ The ID of the dataset to which the data file will be uploaded.
225
+ file_path:
226
+ The file path of the data file to be uploaded.
227
+ verbose:
228
+ If True, prints additional details for debugging. Defaults to True.
229
+
230
+ Raises
231
+ ------
232
+ HTTPError: If the response status code is not 200,
233
+ an HTTPError is raised with the status code and reason.
234
+
235
+ Returns
236
+ -------
237
+ None: The function uploads the file and does not return any value.
238
+
239
+ """
240
+ data_metadata = {"pid": f"doi:{dataset_id}", "filename": file_path.name}
241
+
242
+ df = Datafile()
243
+ df.set(data_metadata)
244
+
245
+ if verbose:
246
+ print(df.get())
247
+
248
+ response = self.api.upload_datafile(f"doi:{dataset_id}", file_path, df.json())
249
+
250
+ if response.status_code not in range(200, 300):
251
+ raise HTTPError(f"{response.status_code}, {response.reason_phrase}")
252
+
253
+ def get_checksum_by_filename(self, dataset_id: str, target_label: str):
254
+ """Retrieve the checksum of a specific file by its label.
255
+
256
+ Parameters
257
+ ----------
258
+ dataset_id:
259
+ The ID of the dataset.
260
+ target_label:
261
+ The name of the file in the dataset.
262
+
263
+ Returns
264
+ -------
265
+ str or None: The checksum value if found, else None.
266
+
267
+ """
268
+ data_dict = self.get_dataset_info(dataset_id)
269
+ files = data_dict["data"]["latestVersion"]["files"]
270
+ for file in files:
271
+ if file.get("label") == target_label:
272
+ return file["dataFile"]["checksum"]["value"]
273
+
274
+ return None
@@ -0,0 +1,226 @@
1
+ """Fetch information and build Dataverse dataset metadata."""
2
+
3
+ import json
4
+ import re
5
+
6
+ DATAVERSE_SUBJECTS = [
7
+ "Agricultural Sciences",
8
+ "Arts and Humanities",
9
+ "Astronomy and Astrophysics",
10
+ "Business and Management",
11
+ "Chemistry",
12
+ "Computer and Information Science",
13
+ "Earth and Environmental Sciences",
14
+ "Engineering",
15
+ "Law",
16
+ "Mathematical Sciences",
17
+ "Medicine, Health and Life Sciences",
18
+ "Physics",
19
+ "Social Sciences",
20
+ "Other",
21
+ ]
22
+
23
+
24
+ # CLI functions
25
+ def get_non_empty_input(prompt):
26
+ """Prompt to screen and check if answer is not empty."""
27
+ while True:
28
+ value = input(prompt).strip()
29
+ if value:
30
+ return value
31
+
32
+
33
+ def collect_authors():
34
+ """Collect authors."""
35
+ authors = []
36
+ while True:
37
+ print("\nEnter author details:")
38
+ # Validate name
39
+ while True:
40
+ name = input(" Contact name (Last, First): ").strip()
41
+ if is_valid_name(name):
42
+ break
43
+ print(" ❌ Invalid format. Use 'Last, First'.")
44
+ affiliation = get_non_empty_input(" Author affiliation: ")
45
+ authors.append(
46
+ {
47
+ "authorName": {
48
+ "value": name,
49
+ "typeClass": "primitive",
50
+ "multiple": False,
51
+ "typeName": "authorName",
52
+ },
53
+ "authorAffiliation": {
54
+ "value": affiliation,
55
+ "typeClass": "primitive",
56
+ "multiple": False,
57
+ "typeName": "authorAffiliation",
58
+ },
59
+ }
60
+ )
61
+ more = input(" Add another author? (y/n): ").strip().lower()
62
+ if more != "y":
63
+ break
64
+ return authors
65
+
66
+
67
+ def collect_contacts():
68
+ """Collect dataset contacts."""
69
+ contacts = []
70
+ while True:
71
+ print("\nEnter dataset contact details:")
72
+ # Validate name
73
+ while True:
74
+ name = input(" Contact name (Last, First): ").strip()
75
+ if is_valid_name(name):
76
+ break
77
+ print(" ❌ Invalid format. Use 'Last, First'.")
78
+
79
+ # Validate email
80
+ while True:
81
+ email = input(" Contact email: ").strip()
82
+ if is_valid_email(email):
83
+ break
84
+ print(" ❌ Invalid email format. Try again.")
85
+
86
+ contacts.append(
87
+ {
88
+ "datasetContactName": {
89
+ "value": name,
90
+ "typeClass": "primitive",
91
+ "multiple": False,
92
+ "typeName": "datasetContactName",
93
+ },
94
+ "datasetContactEmail": {
95
+ "value": email,
96
+ "typeClass": "primitive",
97
+ "multiple": False,
98
+ "typeName": "datasetContactEmail",
99
+ },
100
+ }
101
+ )
102
+ more = input(" Add another contact? (y/n): ").strip().lower()
103
+ if more != "y":
104
+ break
105
+ return contacts
106
+
107
+
108
+ def collect_descriptions():
109
+ """Collect descriptions."""
110
+ descriptions = []
111
+ while True:
112
+ print("\nEnter dataset description:")
113
+ desc = get_non_empty_input(" Description: ")
114
+ descriptions.append(
115
+ {
116
+ "dsDescriptionValue": {
117
+ "value": desc,
118
+ "multiple": False,
119
+ "typeClass": "primitive",
120
+ "typeName": "dsDescriptionValue",
121
+ }
122
+ }
123
+ )
124
+ more = input(" Add another description? (y/n): ").strip().lower()
125
+ if more != "y":
126
+ break
127
+ return descriptions
128
+
129
+
130
+ def collect_subjects():
131
+ """Collect subjects."""
132
+ subjects = []
133
+ print("\nEnter subject(s). Type '?' to list valid subjects.")
134
+ while True:
135
+ subject = input(" Subject: ").strip()
136
+ if subject == "?":
137
+ print(" Valid Dataverse subjects:")
138
+ for s in DATAVERSE_SUBJECTS:
139
+ print(f" - {s}")
140
+ continue
141
+ if subject in DATAVERSE_SUBJECTS:
142
+ if subject not in subjects:
143
+ subjects.append(subject)
144
+ else:
145
+ print(" Already added.")
146
+ else:
147
+ print(" ❌ Invalid subject. Type '?' to see the list of valid subjects.")
148
+ more = input(" Add another subject? (y/n): ").strip().lower()
149
+ if more != "y":
150
+ break
151
+ return subjects
152
+
153
+
154
+ def gather_metadata_inputs():
155
+ """Gather metadata inputs."""
156
+ print("=== Dataset Metadata Entry Tool ===\n")
157
+
158
+ inputs = {
159
+ "title": get_non_empty_input("Enter dataset title: "),
160
+ "authors": collect_authors(),
161
+ "contacts": collect_contacts(),
162
+ "descriptions": collect_descriptions(),
163
+ "subjects": collect_subjects(),
164
+ }
165
+ return inputs
166
+
167
+
168
+ # General functions
169
+ def is_valid_email(email):
170
+ """Check if email is valid using regex."""
171
+ pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$"
172
+ return re.match(pattern, email)
173
+
174
+
175
+ def is_valid_name(name):
176
+ """Check if name is in 'Last, First' format."""
177
+ parts = [p.strip() for p in name.split(",")]
178
+ return len(parts) == 2 and all(parts)
179
+
180
+
181
+ def build_metadata(inputs):
182
+ """Build metadata dictionary from collected inputs."""
183
+ meta = {
184
+ "datasetVersion": {
185
+ "metadataBlocks": {
186
+ "citation": {
187
+ "displayName": "Citation Metadata",
188
+ "fields": [
189
+ {
190
+ "typeName": "title",
191
+ "typeClass": "primitive",
192
+ "multiple": False,
193
+ "value": inputs["title"],
194
+ },
195
+ {
196
+ "typeName": "author",
197
+ "typeClass": "compound",
198
+ "multiple": True,
199
+ "value": inputs["authors"],
200
+ },
201
+ {
202
+ "typeName": "datasetContact",
203
+ "typeClass": "compound",
204
+ "multiple": True,
205
+ "value": inputs["contacts"],
206
+ },
207
+ {
208
+ "typeName": "dsDescription",
209
+ "typeClass": "compound",
210
+ "multiple": True,
211
+ "value": inputs["descriptions"],
212
+ },
213
+ {
214
+ "typeName": "subject",
215
+ "typeClass": "controlledVocabulary",
216
+ "multiple": True,
217
+ "value": inputs["subjects"],
218
+ },
219
+ ],
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ json_string = json.dumps(meta, indent=2)
226
+ return json_string