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.
- ibridgescontrib/ibridgesdvn/__init__.py +38 -0
- ibridgescontrib/ibridgesdvn/dataverse.py +274 -0
- ibridgescontrib/ibridgesdvn/ds_meta.py +226 -0
- ibridgescontrib/ibridgesdvn/dvn_config.py +229 -0
- ibridgescontrib/ibridgesdvn/dvn_dataset_commands.py +294 -0
- ibridgescontrib/ibridgesdvn/dvn_init.py +84 -0
- ibridgescontrib/ibridgesdvn/dvn_operations.py +146 -0
- ibridgescontrib/ibridgesdvn/dvn_setup.py +65 -0
- ibridgescontrib/ibridgesdvn/gui/gui_dataverse.py +280 -0
- ibridgescontrib/ibridgesdvn/gui/gui_popup_widgets.py +321 -0
- ibridgescontrib/ibridgesdvn/gui/gui_thread.py +124 -0
- ibridgescontrib/ibridgesdvn/gui/uiCreateDataset.py +167 -0
- ibridgescontrib/ibridgesdvn/gui/uiCreateMetadata.py +252 -0
- ibridgescontrib/ibridgesdvn/gui/uiCreateUrl.py +132 -0
- ibridgescontrib/ibridgesdvn/gui/uiDataverse.py +287 -0
- ibridgescontrib/ibridgesdvn/gui/ui_files/create_dataset.ui +148 -0
- ibridgescontrib/ibridgesdvn/gui/ui_files/create_url.ui +114 -0
- ibridgescontrib/ibridgesdvn/gui/ui_files/metadata_form.ui +231 -0
- ibridgescontrib/ibridgesdvn/gui/ui_files/tabDataverse.ui +342 -0
- ibridgescontrib/ibridgesdvn/json_templates.py +86 -0
- ibridgescontrib/ibridgesdvn/utils.py +46 -0
- ibridgesdvn-1.0.0.dist-info/METADATA +747 -0
- ibridgesdvn-1.0.0.dist-info/RECORD +27 -0
- ibridgesdvn-1.0.0.dist-info/WHEEL +5 -0
- ibridgesdvn-1.0.0.dist-info/entry_points.txt +13 -0
- ibridgesdvn-1.0.0.dist-info/licenses/LICENSE +504 -0
- ibridgesdvn-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -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
|