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.

Files changed (52) hide show
  1. invenio_vocabularies/__init__.py +1 -1
  2. invenio_vocabularies/administration/views/vocabularies.py +7 -9
  3. invenio_vocabularies/contrib/names/datastreams.py +36 -8
  4. invenio_vocabularies/datastreams/datastreams.py +14 -0
  5. invenio_vocabularies/datastreams/writers.py +6 -0
  6. invenio_vocabularies/jobs.py +11 -11
  7. invenio_vocabularies/records/models.py +4 -1
  8. invenio_vocabularies/services/custom_fields/subject.py +4 -4
  9. invenio_vocabularies/services/tasks.py +7 -1
  10. {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info}/METADATA +9 -6
  11. {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info}/RECORD +16 -52
  12. {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info}/WHEEL +1 -1
  13. invenio_vocabularies/translations/af/LC_MESSAGES/messages.mo +0 -0
  14. invenio_vocabularies/translations/af/LC_MESSAGES/messages.po +0 -139
  15. invenio_vocabularies/translations/de_AT/LC_MESSAGES/messages.mo +0 -0
  16. invenio_vocabularies/translations/de_AT/LC_MESSAGES/messages.po +0 -139
  17. invenio_vocabularies/translations/de_DE/LC_MESSAGES/messages.mo +0 -0
  18. invenio_vocabularies/translations/de_DE/LC_MESSAGES/messages.po +0 -139
  19. invenio_vocabularies/translations/en_AT/LC_MESSAGES/messages.mo +0 -0
  20. invenio_vocabularies/translations/en_AT/LC_MESSAGES/messages.po +0 -139
  21. invenio_vocabularies/translations/en_HU/LC_MESSAGES/messages.mo +0 -0
  22. invenio_vocabularies/translations/en_HU/LC_MESSAGES/messages.po +0 -139
  23. invenio_vocabularies/translations/es_CU/LC_MESSAGES/messages.mo +0 -0
  24. invenio_vocabularies/translations/es_CU/LC_MESSAGES/messages.po +0 -139
  25. invenio_vocabularies/translations/es_MX/LC_MESSAGES/messages.mo +0 -0
  26. invenio_vocabularies/translations/es_MX/LC_MESSAGES/messages.po +0 -139
  27. invenio_vocabularies/translations/et_EE/LC_MESSAGES/messages.mo +0 -0
  28. invenio_vocabularies/translations/et_EE/LC_MESSAGES/messages.po +0 -139
  29. invenio_vocabularies/translations/fa_IR/LC_MESSAGES/messages.mo +0 -0
  30. invenio_vocabularies/translations/fa_IR/LC_MESSAGES/messages.po +0 -139
  31. invenio_vocabularies/translations/fr_CI/LC_MESSAGES/messages.mo +0 -0
  32. invenio_vocabularies/translations/fr_CI/LC_MESSAGES/messages.po +0 -139
  33. invenio_vocabularies/translations/fr_FR/LC_MESSAGES/messages.mo +0 -0
  34. invenio_vocabularies/translations/fr_FR/LC_MESSAGES/messages.po +0 -139
  35. invenio_vocabularies/translations/gl/LC_MESSAGES/messages.mo +0 -0
  36. invenio_vocabularies/translations/gl/LC_MESSAGES/messages.po +0 -139
  37. invenio_vocabularies/translations/hi_IN/LC_MESSAGES/messages.mo +0 -0
  38. invenio_vocabularies/translations/hi_IN/LC_MESSAGES/messages.po +0 -139
  39. invenio_vocabularies/translations/hu_HU/LC_MESSAGES/messages.mo +0 -0
  40. invenio_vocabularies/translations/hu_HU/LC_MESSAGES/messages.po +0 -139
  41. invenio_vocabularies/translations/ne/LC_MESSAGES/messages.mo +0 -0
  42. invenio_vocabularies/translations/ne/LC_MESSAGES/messages.po +0 -139
  43. invenio_vocabularies/translations/rw/LC_MESSAGES/messages.mo +0 -0
  44. invenio_vocabularies/translations/rw/LC_MESSAGES/messages.po +0 -139
  45. invenio_vocabularies/translations/sv_SE/LC_MESSAGES/messages.mo +0 -0
  46. invenio_vocabularies/translations/sv_SE/LC_MESSAGES/messages.po +0 -139
  47. invenio_vocabularies/translations/uk_UA/LC_MESSAGES/messages.mo +0 -0
  48. invenio_vocabularies/translations/uk_UA/LC_MESSAGES/messages.po +0 -139
  49. {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info}/entry_points.txt +0 -0
  50. {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info/licenses}/AUTHORS.rst +0 -0
  51. {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info/licenses}/LICENSE +0 -0
  52. {invenio_vocabularies-7.3.0.dist-info → invenio_vocabularies-7.4.0.dist-info}/top_level.txt +0 -0
@@ -11,6 +11,6 @@
11
11
 
12
12
  from .ext import InvenioVocabularies
13
13
 
14
- __version__ = "7.3.0"
14
+ __version__ = "7.4.0"
15
15
 
16
16
  __all__ = ("__version__", "InvenioVocabularies")
@@ -9,10 +9,8 @@
9
9
 
10
10
  """Vocabularies admin interface."""
11
11
 
12
- from invenio_administration.views.base import (
13
- AdminResourceEditView,
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
- current_app.logger.exception("Failed to fetch ORCiD record.")
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"[ORCID Reader] Processing lambda file...")
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("Name not found in ORCiD entry.")
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("Family name not found in ORCiD entry.")
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("Invalid characters in name.")
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
- pass
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
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  import datetime
12
12
 
13
- from invenio_i18n import gettext as _
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"], f"No '{b}' allowed in VocabularyScheme.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.2
1
+ Metadata-Version: 2.4
2
2
  Name: invenio-vocabularies
3
- Version: 7.3.0
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-2021 CERN.
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