invenio-vocabularies 7.3.0__py2.py3-none-any.whl → 7.4.0__py2.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.
Potentially problematic release.
This version of invenio-vocabularies might be problematic. Click here for more details.
- invenio_vocabularies/__init__.py +1 -1
- invenio_vocabularies/administration/views/vocabularies.py +7 -9
- invenio_vocabularies/contrib/names/datastreams.py +36 -8
- invenio_vocabularies/datastreams/datastreams.py +14 -0
- invenio_vocabularies/datastreams/writers.py +6 -0
- invenio_vocabularies/jobs.py +11 -11
- invenio_vocabularies/records/models.py +4 -1
- invenio_vocabularies/services/custom_fields/subject.py +4 -4
- invenio_vocabularies/services/tasks.py +7 -1
- {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info}/METADATA +9 -6
- {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info}/RECORD +16 -52
- {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info}/WHEEL +1 -1
- invenio_vocabularies/translations/af/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/af/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/de_AT/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/de_AT/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/de_DE/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/de_DE/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/en_AT/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/en_AT/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/en_HU/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/en_HU/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/es_CU/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/es_CU/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/es_MX/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/es_MX/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/et_EE/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/et_EE/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/fa_IR/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/fa_IR/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/fr_CI/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/fr_CI/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/fr_FR/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/fr_FR/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/gl/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/gl/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/hi_IN/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/hi_IN/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/hu_HU/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/hu_HU/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/ne/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/ne/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/rw/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/rw/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/sv_SE/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/sv_SE/LC_MESSAGES/messages.po +0 -139
- invenio_vocabularies/translations/uk_UA/LC_MESSAGES/messages.mo +0 -0
- invenio_vocabularies/translations/uk_UA/LC_MESSAGES/messages.po +0 -139
- {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info}/entry_points.txt +0 -0
- {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info/licenses}/AUTHORS.rst +0 -0
- {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info/licenses}/LICENSE +0 -0
- {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info}/top_level.txt +0 -0
invenio_vocabularies/__init__.py
CHANGED
|
@@ -9,10 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
"""Vocabularies admin interface."""
|
|
11
11
|
|
|
12
|
-
from invenio_administration.views.base import
|
|
13
|
-
|
|
14
|
-
AdminResourceListView,
|
|
15
|
-
)
|
|
12
|
+
from invenio_administration.views.base import AdminResourceListView
|
|
13
|
+
from invenio_i18n import lazy_gettext as _
|
|
16
14
|
|
|
17
15
|
|
|
18
16
|
class VocabulariesListView(AdminResourceListView):
|
|
@@ -20,11 +18,11 @@ class VocabulariesListView(AdminResourceListView):
|
|
|
20
18
|
|
|
21
19
|
api_endpoint = "/vocabularies/"
|
|
22
20
|
name = "vocabulary-types"
|
|
23
|
-
menu_label = "Vocabulary Types"
|
|
21
|
+
menu_label = _("Vocabulary Types")
|
|
24
22
|
resource_config = "vocabulary_admin_resource"
|
|
25
23
|
search_request_headers = {"Accept": "application/json"}
|
|
26
|
-
title = "Vocabulary Types"
|
|
27
|
-
category = "Site management"
|
|
24
|
+
title = _("Vocabulary Types")
|
|
25
|
+
category = _("Site management")
|
|
28
26
|
|
|
29
27
|
pid_path = "id"
|
|
30
28
|
icon = "exchange"
|
|
@@ -36,8 +34,8 @@ class VocabulariesListView(AdminResourceListView):
|
|
|
36
34
|
display_create = False
|
|
37
35
|
|
|
38
36
|
item_field_list = {
|
|
39
|
-
"id": {"text": "Name", "order": 1},
|
|
40
|
-
"count": {"text": "Number of entries", "order": 2},
|
|
37
|
+
"id": {"text": _("Name"), "order": 1},
|
|
38
|
+
"count": {"text": _("Number of entries"), "order": 2},
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
search_config_name = "VOCABULARIES_TYPES_SEARCH"
|
|
@@ -12,6 +12,7 @@ import csv
|
|
|
12
12
|
import io
|
|
13
13
|
import tarfile
|
|
14
14
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
15
|
+
from contextvars import copy_context
|
|
15
16
|
from datetime import timedelta
|
|
16
17
|
from itertools import islice
|
|
17
18
|
from pathlib import Path
|
|
@@ -43,17 +44,18 @@ class OrcidDataSyncReader(BaseReader):
|
|
|
43
44
|
self.s3_client = S3OrcidClient()
|
|
44
45
|
self.since = since
|
|
45
46
|
|
|
46
|
-
def _fetch_orcid_data(self, orcid_to_sync, bucket):
|
|
47
|
+
def _fetch_orcid_data(self, app, orcid_to_sync, bucket):
|
|
47
48
|
"""Fetches a single ORCiD record from S3."""
|
|
48
49
|
# The ORCiD file key is located in a folder which name corresponds to the last three digits of the ORCiD
|
|
49
50
|
suffix = orcid_to_sync[-3:]
|
|
50
51
|
key = f"{suffix}/{orcid_to_sync}.xml"
|
|
52
|
+
app.logger.debug(f"Fetching ORCiD record: {key} from bucket: {bucket}")
|
|
51
53
|
try:
|
|
52
54
|
# Potential improvement: use the a XML jax parser to avoid loading the whole file in memory
|
|
53
55
|
# and choose the sections we need to read (probably the summary)
|
|
54
56
|
return self.s3_client.read_file(f"s3://{bucket}/{key}")
|
|
55
57
|
except Exception:
|
|
56
|
-
|
|
58
|
+
app.logger.exception(f"Failed to fetch ORCiD record: {key}")
|
|
57
59
|
|
|
58
60
|
def _process_lambda_file(self, fileobj):
|
|
59
61
|
"""Process the ORCiD lambda file and returns a list of ORCiDs to sync.
|
|
@@ -87,7 +89,11 @@ class OrcidDataSyncReader(BaseReader):
|
|
|
87
89
|
)
|
|
88
90
|
|
|
89
91
|
if last_modified_date < last_sync:
|
|
92
|
+
current_app.logger.debug(
|
|
93
|
+
f"Skipping ORCiD {orcid} (last modified: {last_modified_date})"
|
|
94
|
+
)
|
|
90
95
|
break
|
|
96
|
+
current_app.logger.debug(f"Yielding ORCiD {orcid} for sync.")
|
|
91
97
|
yield orcid
|
|
92
98
|
finally:
|
|
93
99
|
fileobj.close()
|
|
@@ -97,10 +103,15 @@ class OrcidDataSyncReader(BaseReader):
|
|
|
97
103
|
with ThreadPoolExecutor(
|
|
98
104
|
max_workers=current_app.config["VOCABULARIES_ORCID_SYNC_MAX_WORKERS"]
|
|
99
105
|
) as executor:
|
|
106
|
+
app = current_app._get_current_object()
|
|
100
107
|
# futures is a dictionary where the key is the ORCID value and the item is the Future object
|
|
108
|
+
# Flask does not propagate app/request context to new threads, so `copy_context().run`
|
|
109
|
+
# ensures the current instantianted contextvars (such as job_context) is preserved in each thread.
|
|
101
110
|
futures = {
|
|
102
111
|
orcid: executor.submit(
|
|
112
|
+
copy_context().run, # Required to pass the context to the thread
|
|
103
113
|
self._fetch_orcid_data,
|
|
114
|
+
app, # Pass the Flask app to the thread
|
|
104
115
|
orcid,
|
|
105
116
|
current_app.config["VOCABULARIES_ORCID_SUMMARIES_BUCKET"],
|
|
106
117
|
)
|
|
@@ -111,7 +122,14 @@ class OrcidDataSyncReader(BaseReader):
|
|
|
111
122
|
try:
|
|
112
123
|
result = futures[orcid].result()
|
|
113
124
|
if result:
|
|
125
|
+
current_app.logger.debug(
|
|
126
|
+
f"Successfully fetched ORCiD record: {orcid}"
|
|
127
|
+
)
|
|
114
128
|
yield result
|
|
129
|
+
except Exception:
|
|
130
|
+
current_app.logger.exception(
|
|
131
|
+
f"Error processing ORCiD record: {orcid}"
|
|
132
|
+
)
|
|
115
133
|
finally:
|
|
116
134
|
# Explicitly release memory, as we don't need the future anymore.
|
|
117
135
|
# This is mostly required because as long as we keep a reference to the future
|
|
@@ -125,7 +143,7 @@ class OrcidDataSyncReader(BaseReader):
|
|
|
125
143
|
tar_content = self.s3_client.read_file(
|
|
126
144
|
"s3://orcid-lambda-file/last_modified.csv.tar"
|
|
127
145
|
)
|
|
128
|
-
|
|
146
|
+
current_app.logger.info("Fetching ORCiD lambda file")
|
|
129
147
|
# Opens tar file and process it
|
|
130
148
|
with tarfile.open(fileobj=io.BytesIO(tar_content)) as tar:
|
|
131
149
|
# Iterate over each member (file or directory) in the tar file
|
|
@@ -133,7 +151,7 @@ class OrcidDataSyncReader(BaseReader):
|
|
|
133
151
|
# Extract the file
|
|
134
152
|
extracted_file = tar.extractfile(member)
|
|
135
153
|
if extracted_file:
|
|
136
|
-
current_app.logger.info(f"
|
|
154
|
+
current_app.logger.info(f"Processing lambda file: {member.name}")
|
|
137
155
|
# Process the file and get the ORCiDs to sync
|
|
138
156
|
orcids_to_sync = set(self._process_lambda_file(extracted_file))
|
|
139
157
|
|
|
@@ -150,6 +168,7 @@ class OrcidDataSyncReader(BaseReader):
|
|
|
150
168
|
"""Yield successive chunks of a given size."""
|
|
151
169
|
it = iter(iterable)
|
|
152
170
|
while chunk := list(islice(it, batch_size)):
|
|
171
|
+
current_app.logger.debug(f"Processing batch of size {len(chunk)}.")
|
|
153
172
|
yield chunk
|
|
154
173
|
|
|
155
174
|
|
|
@@ -239,18 +258,25 @@ class OrcidTransformer(BaseTransformer):
|
|
|
239
258
|
|
|
240
259
|
def apply(self, stream_entry, **kwargs):
|
|
241
260
|
"""Applies the transformation to the stream entry."""
|
|
261
|
+
current_app.logger.debug("Applying transformation to stream entry.")
|
|
242
262
|
record = stream_entry.entry
|
|
243
263
|
person = record["person"]
|
|
244
264
|
orcid_id = record["orcid-identifier"]["path"]
|
|
245
265
|
|
|
246
266
|
name = person.get("name")
|
|
247
267
|
if name is None:
|
|
248
|
-
raise TransformerError(
|
|
268
|
+
raise TransformerError(
|
|
269
|
+
f"Name not found in ORCiD entry for ORCiD ID: {orcid_id}."
|
|
270
|
+
)
|
|
249
271
|
if name.get("family-name") is None:
|
|
250
|
-
raise TransformerError(
|
|
272
|
+
raise TransformerError(
|
|
273
|
+
f"Family name not found in ORCiD entry for ORCiD ID: {orcid_id}."
|
|
274
|
+
)
|
|
251
275
|
|
|
252
276
|
if not self._is_valid_name(name["given-names"] + name["family-name"]):
|
|
253
|
-
raise TransformerError(
|
|
277
|
+
raise TransformerError(
|
|
278
|
+
f"Invalid characters in name for ORCiD ID: {orcid_id}."
|
|
279
|
+
)
|
|
254
280
|
|
|
255
281
|
entry = {
|
|
256
282
|
"id": orcid_id,
|
|
@@ -261,6 +287,7 @@ class OrcidTransformer(BaseTransformer):
|
|
|
261
287
|
}
|
|
262
288
|
|
|
263
289
|
stream_entry.entry = entry
|
|
290
|
+
current_app.logger.debug(f"Transformed entry: {entry}")
|
|
264
291
|
return stream_entry
|
|
265
292
|
|
|
266
293
|
def _is_valid_name(self, name):
|
|
@@ -271,6 +298,7 @@ class OrcidTransformer(BaseTransformer):
|
|
|
271
298
|
|
|
272
299
|
def _extract_affiliations(self, record):
|
|
273
300
|
"""Extract affiliations from the ORCiD record."""
|
|
301
|
+
current_app.logger.debug("Extracting affiliations from ORCiD record.")
|
|
274
302
|
result = []
|
|
275
303
|
try:
|
|
276
304
|
employments = (
|
|
@@ -312,7 +340,7 @@ class OrcidTransformer(BaseTransformer):
|
|
|
312
340
|
|
|
313
341
|
result.append(aff)
|
|
314
342
|
except Exception:
|
|
315
|
-
|
|
343
|
+
current_app.logger.error("Error extracting affiliations.")
|
|
316
344
|
return result
|
|
317
345
|
|
|
318
346
|
def _extract_affiliation_id(self, org):
|
|
@@ -72,13 +72,18 @@ class DataStream:
|
|
|
72
72
|
|
|
73
73
|
def filter(self, stream_entry, *args, **kwargs):
|
|
74
74
|
"""Checks if an stream_entry should be filtered out (skipped)."""
|
|
75
|
+
current_app.logger.debug(f"Filtering entry: {stream_entry.entry}")
|
|
75
76
|
return False
|
|
76
77
|
|
|
77
78
|
def process_batch(self, batch):
|
|
78
79
|
"""Process a batch of entries."""
|
|
80
|
+
current_app.logger.info(f"Processing batch of size: {len(batch)}")
|
|
79
81
|
transformed_entries = []
|
|
80
82
|
for stream_entry in batch:
|
|
81
83
|
if stream_entry.errors:
|
|
84
|
+
current_app.logger.warning(
|
|
85
|
+
f"Skipping entry with errors: {stream_entry.errors}"
|
|
86
|
+
)
|
|
82
87
|
yield stream_entry # reading errors
|
|
83
88
|
else:
|
|
84
89
|
transformed_entry = self.transform(stream_entry)
|
|
@@ -103,19 +108,23 @@ class DataStream:
|
|
|
103
108
|
the reader, apply the transformations and yield the result of
|
|
104
109
|
writing it.
|
|
105
110
|
"""
|
|
111
|
+
current_app.logger.info("Starting data stream processing")
|
|
106
112
|
batch = []
|
|
107
113
|
for stream_entry in self.read():
|
|
108
114
|
batch.append(stream_entry)
|
|
109
115
|
if len(batch) >= self.batch_size:
|
|
116
|
+
current_app.logger.debug(f"Processing batch of size: {len(batch)}")
|
|
110
117
|
yield from self.process_batch(batch)
|
|
111
118
|
batch = []
|
|
112
119
|
|
|
113
120
|
# Process any remaining entries in the last batch
|
|
114
121
|
if batch:
|
|
122
|
+
current_app.logger.debug(f"Processing final batch of size: {len(batch)}")
|
|
115
123
|
yield from self.process_batch(batch)
|
|
116
124
|
|
|
117
125
|
def read(self):
|
|
118
126
|
"""Recursively read the entries."""
|
|
127
|
+
current_app.logger.debug("Reading entries from readers")
|
|
119
128
|
|
|
120
129
|
def pipe_gen(gen_funcs, piped_item=None):
|
|
121
130
|
_gen_funcs = list(gen_funcs) # copy to avoid modifying ref list
|
|
@@ -130,6 +139,7 @@ class DataStream:
|
|
|
130
139
|
else:
|
|
131
140
|
yield StreamEntry(item)
|
|
132
141
|
except ReaderError as err:
|
|
142
|
+
current_app.logger.error(f"Reader error: {str(err)}")
|
|
133
143
|
yield StreamEntry(
|
|
134
144
|
entry=item,
|
|
135
145
|
errors=[f"{current_gen_func.__qualname__}: {str(err)}"],
|
|
@@ -140,6 +150,7 @@ class DataStream:
|
|
|
140
150
|
|
|
141
151
|
def transform(self, stream_entry, *args, **kwargs):
|
|
142
152
|
"""Apply the transformations to an stream_entry."""
|
|
153
|
+
current_app.logger.debug(f"Transforming entry: {stream_entry.entry}")
|
|
143
154
|
for transformer in self._transformers:
|
|
144
155
|
try:
|
|
145
156
|
stream_entry = transformer.apply(stream_entry)
|
|
@@ -153,16 +164,19 @@ class DataStream:
|
|
|
153
164
|
|
|
154
165
|
def write(self, stream_entry, *args, **kwargs):
|
|
155
166
|
"""Apply the transformations to an stream_entry."""
|
|
167
|
+
current_app.logger.debug(f"Writing entry: {stream_entry.entry}")
|
|
156
168
|
for writer in self._writers:
|
|
157
169
|
try:
|
|
158
170
|
writer.write(stream_entry)
|
|
159
171
|
except WriterError as err:
|
|
172
|
+
current_app.logger.error(f"Writer error: {str(err)}")
|
|
160
173
|
stream_entry.errors.append(f"{writer.__class__.__name__}: {str(err)}")
|
|
161
174
|
|
|
162
175
|
return stream_entry
|
|
163
176
|
|
|
164
177
|
def batch_write(self, stream_entries, *args, **kwargs):
|
|
165
178
|
"""Apply the transformations to an stream_entry. Errors are handler in the service layer."""
|
|
179
|
+
current_app.logger.debug(f"Batch writing entries: {len(stream_entries)}")
|
|
166
180
|
for writer in self._writers:
|
|
167
181
|
yield from writer.write_many(stream_entries)
|
|
168
182
|
|
|
@@ -87,17 +87,21 @@ class ServiceWriter(BaseWriter):
|
|
|
87
87
|
|
|
88
88
|
def _do_update(self, entry):
|
|
89
89
|
vocab_id = self._entry_id(entry)
|
|
90
|
+
current_app.logger.debug(f"Resolving entry with ID: {vocab_id}")
|
|
90
91
|
current = self._resolve(vocab_id)
|
|
91
92
|
updated = dict(current.to_dict(), **entry)
|
|
93
|
+
current_app.logger.debug(f"Updating entry with ID: {vocab_id}")
|
|
92
94
|
return StreamEntry(self._service.update(self._identity, vocab_id, updated))
|
|
93
95
|
|
|
94
96
|
def write(self, stream_entry, *args, **kwargs):
|
|
95
97
|
"""Writes the input entry using a given service."""
|
|
96
98
|
entry = stream_entry.entry
|
|
99
|
+
current_app.logger.debug(f"Writing entry: {entry}")
|
|
97
100
|
|
|
98
101
|
try:
|
|
99
102
|
if self._insert:
|
|
100
103
|
try:
|
|
104
|
+
current_app.logger.debug("Inserting entry.")
|
|
101
105
|
return StreamEntry(self._service.create(self._identity, entry))
|
|
102
106
|
except PIDAlreadyExists:
|
|
103
107
|
if not self._update:
|
|
@@ -105,6 +109,7 @@ class ServiceWriter(BaseWriter):
|
|
|
105
109
|
return self._do_update(entry)
|
|
106
110
|
elif self._update:
|
|
107
111
|
try:
|
|
112
|
+
current_app.logger.debug("Attempting to update entry.")
|
|
108
113
|
return self._do_update(entry)
|
|
109
114
|
except (NoResultFound, PIDDoesNotExistError):
|
|
110
115
|
raise WriterError([f"Vocabulary entry does not exist: {entry}"])
|
|
@@ -139,6 +144,7 @@ class ServiceWriter(BaseWriter):
|
|
|
139
144
|
processed_stream_entry.log_errors()
|
|
140
145
|
stream_entries_processed.append(processed_stream_entry)
|
|
141
146
|
|
|
147
|
+
current_app.logger.debug(f"Finished writing {len(stream_entries)} entries")
|
|
142
148
|
return stream_entries_processed
|
|
143
149
|
|
|
144
150
|
|
invenio_vocabularies/jobs.py
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import datetime
|
|
12
12
|
|
|
13
|
-
from invenio_i18n import
|
|
13
|
+
from invenio_i18n import lazy_gettext as _
|
|
14
14
|
from invenio_jobs.jobs import JobType
|
|
15
15
|
|
|
16
16
|
from invenio_vocabularies.services.tasks import process_datastream
|
|
@@ -27,8 +27,8 @@ class ProcessDataStreamJob(JobType):
|
|
|
27
27
|
class ProcessRORAffiliationsJob(ProcessDataStreamJob):
|
|
28
28
|
"""Process ROR affiliations datastream registered task."""
|
|
29
29
|
|
|
30
|
-
description = "Process ROR affiliations"
|
|
31
|
-
title = "Load ROR affiliations"
|
|
30
|
+
description = _("Process ROR affiliations")
|
|
31
|
+
title = _("Load ROR affiliations")
|
|
32
32
|
id = "process_ror_affiliations"
|
|
33
33
|
|
|
34
34
|
@classmethod
|
|
@@ -65,8 +65,8 @@ class ProcessRORAffiliationsJob(ProcessDataStreamJob):
|
|
|
65
65
|
class ProcessRORFundersJob(ProcessDataStreamJob):
|
|
66
66
|
"""Process ROR funders datastream registered task."""
|
|
67
67
|
|
|
68
|
-
description = "Process ROR funders"
|
|
69
|
-
title = "Load ROR funders"
|
|
68
|
+
description = _("Process ROR funders")
|
|
69
|
+
title = _("Load ROR funders")
|
|
70
70
|
id = "process_ror_funders"
|
|
71
71
|
|
|
72
72
|
@classmethod
|
|
@@ -103,8 +103,8 @@ class ProcessRORFundersJob(ProcessDataStreamJob):
|
|
|
103
103
|
class ImportAwardsOpenAIREJob(ProcessDataStreamJob):
|
|
104
104
|
"""Import awards from OpenAIRE registered task."""
|
|
105
105
|
|
|
106
|
-
description = "Import awards from OpenAIRE"
|
|
107
|
-
title = "Import Awards OpenAIRE"
|
|
106
|
+
description = _("Import awards from OpenAIRE")
|
|
107
|
+
title = _("Import Awards OpenAIRE")
|
|
108
108
|
id = "import_awards_openaire"
|
|
109
109
|
|
|
110
110
|
@classmethod
|
|
@@ -138,8 +138,8 @@ class ImportAwardsOpenAIREJob(ProcessDataStreamJob):
|
|
|
138
138
|
class UpdateAwardsCordisJob(ProcessDataStreamJob):
|
|
139
139
|
"""Update awards from CORDIS registered task."""
|
|
140
140
|
|
|
141
|
-
description = "Update awards from CORDIS"
|
|
142
|
-
title = "Update Awards CORDIS"
|
|
141
|
+
description = _("Update awards from CORDIS")
|
|
142
|
+
title = _("Update Awards CORDIS")
|
|
143
143
|
id = "update_awards_cordis"
|
|
144
144
|
|
|
145
145
|
@classmethod
|
|
@@ -166,8 +166,8 @@ class UpdateAwardsCordisJob(ProcessDataStreamJob):
|
|
|
166
166
|
class ImportORCIDJob(ProcessDataStreamJob):
|
|
167
167
|
"""Import ORCID data registered task."""
|
|
168
168
|
|
|
169
|
-
description = "Import ORCID data"
|
|
170
|
-
title = "Import ORCID data"
|
|
169
|
+
description = _("Import ORCID data")
|
|
170
|
+
title = _("Import ORCID data")
|
|
171
171
|
id = "import_orcid"
|
|
172
172
|
|
|
173
173
|
@classmethod
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"""Vocabulary models."""
|
|
10
10
|
|
|
11
11
|
from invenio_db import db
|
|
12
|
+
from invenio_i18n import gettext as _
|
|
12
13
|
from invenio_records.models import RecordMetadataBase
|
|
13
14
|
|
|
14
15
|
|
|
@@ -79,7 +80,9 @@ class VocabularyScheme(db.Model):
|
|
|
79
80
|
"""Create a new vocabulary subtype."""
|
|
80
81
|
banned = [",", ":"]
|
|
81
82
|
for b in banned:
|
|
82
|
-
assert b not in data["id"],
|
|
83
|
+
assert b not in data["id"], _(
|
|
84
|
+
"No '%(banned_char)s' allowed in VocabularyScheme.id", banned_char=b
|
|
85
|
+
)
|
|
83
86
|
|
|
84
87
|
with db.session.begin_nested():
|
|
85
88
|
obj = cls(**data)
|
|
@@ -54,12 +54,12 @@ SUBJECT_FIELDS_UI = [
|
|
|
54
54
|
ui_widget="SubjectAutocompleteDropdown",
|
|
55
55
|
isGenericVocabulary=False,
|
|
56
56
|
props=dict(
|
|
57
|
-
label="Keywords and subjects",
|
|
57
|
+
label=_("Keywords and subjects"),
|
|
58
58
|
icon="tag",
|
|
59
|
-
description="The subjects related to the community",
|
|
60
|
-
placeholder="Search for a subject by name e.g. Psychology ...",
|
|
59
|
+
description=_("The subjects related to the community"),
|
|
60
|
+
placeholder=_("Search for a subject by name e.g. Psychology ..."),
|
|
61
61
|
autocompleteFrom="api/subjects",
|
|
62
|
-
noQueryMessage="Search for subjects...",
|
|
62
|
+
noQueryMessage=_("Search for subjects..."),
|
|
63
63
|
autocompleteFromAcceptHeader="application/vnd.inveniordm.v1+json",
|
|
64
64
|
required=False,
|
|
65
65
|
multiple=True,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
from celery import shared_task
|
|
11
11
|
from flask import current_app
|
|
12
|
+
from invenio_jobs.errors import TaskExecutionError
|
|
12
13
|
|
|
13
14
|
from ..datastreams.factories import DataStreamFactory
|
|
14
15
|
|
|
@@ -23,8 +24,13 @@ def process_datastream(config):
|
|
|
23
24
|
batch_size=config.get("batch_size", 1000),
|
|
24
25
|
write_many=config.get("write_many", False),
|
|
25
26
|
)
|
|
26
|
-
|
|
27
|
+
entries_with_errors = 0
|
|
27
28
|
for result in ds.process():
|
|
28
29
|
if result.errors:
|
|
29
30
|
for err in result.errors:
|
|
30
31
|
current_app.logger.error(err)
|
|
32
|
+
entries_with_errors += 1
|
|
33
|
+
if entries_with_errors:
|
|
34
|
+
raise TaskExecutionError(
|
|
35
|
+
message=f"Task execution succeeded with {entries_with_errors} entries with errors."
|
|
36
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: invenio-vocabularies
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.4.0
|
|
4
4
|
Summary: Invenio module for managing vocabularies.
|
|
5
5
|
Home-page: https://github.com/inveniosoftware/invenio-vocabularies
|
|
6
6
|
Author: CERN
|
|
@@ -45,9 +45,10 @@ Requires-Dist: invenio-search[opensearch2]<4.0.0,>=3.0.0; extra == "opensearch2"
|
|
|
45
45
|
Provides-Extra: mysql
|
|
46
46
|
Provides-Extra: postgresql
|
|
47
47
|
Provides-Extra: sqlite
|
|
48
|
+
Dynamic: license-file
|
|
48
49
|
|
|
49
50
|
..
|
|
50
|
-
Copyright (C) 2020-
|
|
51
|
+
Copyright (C) 2020-2025 CERN.
|
|
51
52
|
|
|
52
53
|
Invenio-Vocabularies is free software; you can redistribute it and/or
|
|
53
54
|
modify it under the terms of the MIT License; see LICENSE file for more
|
|
@@ -77,9 +78,6 @@ Invenio module for managing vocabularies, based on Invenio-Records and Invenio-R
|
|
|
77
78
|
- Factories for easily generating models, record API classes, services, and resources
|
|
78
79
|
- Helpers for importing vocabularies
|
|
79
80
|
|
|
80
|
-
Further documentation is available on
|
|
81
|
-
https://invenio-vocabularies.readthedocs.io/
|
|
82
|
-
|
|
83
81
|
..
|
|
84
82
|
Copyright (C) 2020-2024 CERN.
|
|
85
83
|
Copyright (C) 2024 Graz University of Technology.
|
|
@@ -91,6 +89,11 @@ https://invenio-vocabularies.readthedocs.io/
|
|
|
91
89
|
Changes
|
|
92
90
|
=======
|
|
93
91
|
|
|
92
|
+
Version v7.4.0 (released 2025-04-28)
|
|
93
|
+
|
|
94
|
+
- i18n: Fix untranslated strings in vocabularies
|
|
95
|
+
- logging: add basic logging for ORCID
|
|
96
|
+
|
|
94
97
|
Version v7.3.0 (released 2025-03-18)
|
|
95
98
|
|
|
96
99
|
- form: funding: use FeedbackLabel and add error styling
|