invenio-vocabularies 4.2.0__py2.py3-none-any.whl → 4.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 (23) hide show
  1. invenio_vocabularies/__init__.py +1 -1
  2. invenio_vocabularies/cli.py +12 -3
  3. invenio_vocabularies/config.py +13 -0
  4. invenio_vocabularies/contrib/affiliations/affiliations.py +10 -0
  5. invenio_vocabularies/contrib/affiliations/datastreams.py +1 -1
  6. invenio_vocabularies/contrib/funders/datastreams.py +1 -1
  7. invenio_vocabularies/contrib/names/datastreams.py +160 -2
  8. invenio_vocabularies/contrib/names/names.py +10 -0
  9. invenio_vocabularies/contrib/names/s3client.py +44 -0
  10. invenio_vocabularies/contrib/subjects/subjects.py +10 -0
  11. invenio_vocabularies/datastreams/datastreams.py +61 -13
  12. invenio_vocabularies/datastreams/readers.py +8 -2
  13. invenio_vocabularies/datastreams/tasks.py +14 -2
  14. invenio_vocabularies/datastreams/writers.py +53 -2
  15. invenio_vocabularies/services/custom_fields/subject.py +1 -1
  16. invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/subjects.html +1 -1
  17. {invenio_vocabularies-4.2.0.dist-info → invenio_vocabularies-4.4.0.dist-info}/METADATA +14 -1
  18. {invenio_vocabularies-4.2.0.dist-info → invenio_vocabularies-4.4.0.dist-info}/RECORD +23 -22
  19. {invenio_vocabularies-4.2.0.dist-info → invenio_vocabularies-4.4.0.dist-info}/AUTHORS.rst +0 -0
  20. {invenio_vocabularies-4.2.0.dist-info → invenio_vocabularies-4.4.0.dist-info}/LICENSE +0 -0
  21. {invenio_vocabularies-4.2.0.dist-info → invenio_vocabularies-4.4.0.dist-info}/WHEEL +0 -0
  22. {invenio_vocabularies-4.2.0.dist-info → invenio_vocabularies-4.4.0.dist-info}/entry_points.txt +0 -0
  23. {invenio_vocabularies-4.2.0.dist-info → invenio_vocabularies-4.4.0.dist-info}/top_level.txt +0 -0
@@ -10,6 +10,6 @@
10
10
 
11
11
  from .ext import InvenioVocabularies
12
12
 
13
- __version__ = "4.2.0"
13
+ __version__ = "4.4.0"
14
14
 
15
15
  __all__ = ("__version__", "InvenioVocabularies")
@@ -142,18 +142,27 @@ def convert(vocabulary, filepath=None, origin=None, target=None, num_samples=Non
142
142
  type=click.STRING,
143
143
  help="Identifier of the vocabulary item to delete.",
144
144
  )
145
- @click.option("--all", is_flag=True, default=False, help="Not supported yet.")
145
+ @click.option("--all", is_flag=True, default=False)
146
146
  @with_appcontext
147
147
  def delete(vocabulary, identifier, all):
148
148
  """Delete all items or a specific one of the vocabulary."""
149
- if not id and not all:
149
+ if not identifier and not all:
150
150
  click.secho("An identifier or the --all flag must be present.", fg="red")
151
151
  exit(1)
152
+
152
153
  vc = get_vocabulary_config(vocabulary)
153
154
  service = vc.get_service()
154
155
  if identifier:
155
156
  try:
156
- if service.delete(identifier, system_identity):
157
+ if service.delete(system_identity, identifier):
157
158
  click.secho(f"{identifier} deleted from {vocabulary}.", fg="green")
158
159
  except (PIDDeletedError, PIDDoesNotExistError):
159
160
  click.secho(f"PID {identifier} not found.")
161
+ elif all:
162
+ items = service.scan(system_identity)
163
+ for item in items.hits:
164
+ try:
165
+ if service.delete(system_identity, item["id"]):
166
+ click.secho(f"{item['id']} deleted from {vocabulary}.", fg="green")
167
+ except (PIDDeletedError, PIDDoesNotExistError):
168
+ click.secho(f"PID {item['id']} not found.")
@@ -155,3 +155,16 @@ VOCABULARIES_TYPES_SEARCH = {
155
155
  "sort": ["name", "count"],
156
156
  }
157
157
  """Vocabulary type search configuration."""
158
+
159
+ VOCABULARIES_ORCID_ACCESS_KEY = "TODO"
160
+ """ORCID access key to access the s3 bucket."""
161
+ VOCABULARIES_ORCID_SECRET_KEY = "TODO"
162
+ """ORCID secret key to access the s3 bucket."""
163
+ VOCABULARIES_ORCID_SUMMARIES_BUCKET = "v3.0-summaries"
164
+ """ORCID summaries bucket name."""
165
+ VOCABULARIES_ORCID_SYNC_MAX_WORKERS = 32
166
+ """ORCID max number of simultaneous workers/connections."""
167
+ VOCABULARIES_ORCID_SYNC_SINCE = {
168
+ "days": 1,
169
+ }
170
+ """ORCID time shift to sync. Parameters accepted are the ones passed to 'datetime.timedelta'."""
@@ -8,11 +8,13 @@
8
8
 
9
9
  """Vocabulary affiliations."""
10
10
 
11
+ from flask_resources import JSONSerializer, ResponseHandler
11
12
  from invenio_db import db
12
13
  from invenio_records.dumpers import SearchDumper
13
14
  from invenio_records.dumpers.indexedat import IndexedAtDumperExt
14
15
  from invenio_records_resources.factories.factory import RecordTypeFactory
15
16
  from invenio_records_resources.records.systemfields import ModelPIDField
17
+ from invenio_records_resources.resources.records.headers import etag_headers
16
18
 
17
19
  from ...services.permissions import PermissionPolicy
18
20
  from .config import AffiliationsSearchOptions, service_components
@@ -46,4 +48,12 @@ record_type = RecordTypeFactory(
46
48
  permission_policy_cls=PermissionPolicy,
47
49
  # Resource layer
48
50
  endpoint_route="/affiliations",
51
+ resource_cls_attrs={
52
+ "response_handlers": {
53
+ "application/json": ResponseHandler(JSONSerializer(), headers=etag_headers),
54
+ "application/vnd.inveniordm.v1+json": ResponseHandler(
55
+ JSONSerializer(), headers=etag_headers
56
+ ),
57
+ }
58
+ },
49
59
  )
@@ -42,7 +42,7 @@ class AffiliationsRORTransformer(RORTransformer):
42
42
  *args,
43
43
  vocab_schemes=vocab_schemes,
44
44
  funder_fundref_doi_prefix=funder_fundref_doi_prefix,
45
- **kwargs
45
+ **kwargs,
46
46
  )
47
47
 
48
48
 
@@ -46,7 +46,7 @@ class FundersRORTransformer(RORTransformer):
46
46
  *args,
47
47
  vocab_schemes=vocab_schemes,
48
48
  funder_fundref_doi_prefix=funder_fundref_doi_prefix,
49
- **kwargs
49
+ **kwargs,
50
50
  )
51
51
 
52
52
 
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright (C) 2021-2022 CERN.
3
+ # Copyright (C) 2021-2024 CERN.
4
4
  #
5
5
  # Invenio-Vocabularies is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the MIT License; see LICENSE file for more
@@ -8,15 +8,123 @@
8
8
 
9
9
  """Names datastreams, transformers, writers and readers."""
10
10
 
11
+ import csv
12
+ import io
13
+ import tarfile
14
+ from concurrent.futures import ThreadPoolExecutor, as_completed
15
+ from datetime import timedelta
16
+
17
+ import arrow
18
+ import regex as re
19
+ from flask import current_app
11
20
  from invenio_access.permissions import system_identity
12
21
  from invenio_records.dictutils import dict_lookup
13
22
 
23
+ from invenio_vocabularies.contrib.names.s3client import S3OrcidClient
24
+
14
25
  from ...datastreams.errors import TransformerError
15
- from ...datastreams.readers import SimpleHTTPReader
26
+ from ...datastreams.readers import BaseReader, SimpleHTTPReader
16
27
  from ...datastreams.transformers import BaseTransformer
17
28
  from ...datastreams.writers import ServiceWriter
18
29
 
19
30
 
31
+ class OrcidDataSyncReader(BaseReader):
32
+ """ORCiD Data Sync Reader."""
33
+
34
+ def __init__(self, origin=None, mode="r", since=None, *args, **kwargs):
35
+ """Constructor.
36
+
37
+ :param origin: Data source (e.g. filepath).
38
+ Can be none in case of piped readers.
39
+ """
40
+ super().__init__(origin=origin, mode=mode, *args, **kwargs)
41
+ self.s3_client = S3OrcidClient()
42
+ self.since = since
43
+
44
+ def _fetch_orcid_data(self, orcid_to_sync, bucket):
45
+ """Fetches a single ORCiD record from S3."""
46
+ # The ORCiD file key is located in a folder which name corresponds to the last three digits of the ORCiD
47
+ suffix = orcid_to_sync[-3:]
48
+ key = f"{suffix}/{orcid_to_sync}.xml"
49
+ try:
50
+ return self.s3_client.read_file(f"s3://{bucket}/{key}")
51
+ except Exception as e:
52
+ # TODO: log
53
+ return None
54
+
55
+ def _process_lambda_file(self, fileobj):
56
+ """Process the ORCiD lambda file and returns a list of ORCiDs to sync.
57
+
58
+ The decoded fileobj looks like the following:
59
+ orcid, path, date_created, last_modified
60
+ 0000-0001-5109-3700, http://orcid.org/0000-0001-5109-3700, 2014-08-02 15:00:00.000,2021-08-02 15:00:00.000
61
+
62
+ Yield ORCiDs to sync until the last sync date is reached.
63
+ """
64
+ date_format = "YYYY-MM-DD HH:mm:ss.SSSSSS"
65
+ date_format_no_millis = "YYYY-MM-DD HH:mm:ss"
66
+ time_shift = current_app.config["VOCABULARIES_ORCID_SYNC_SINCE"]
67
+ if self.since:
68
+ time_shift = self.since
69
+ last_sync = arrow.now() - timedelta(**time_shift)
70
+
71
+ file_content = fileobj.read().decode("utf-8")
72
+
73
+ csv_reader = csv.DictReader(file_content.splitlines())
74
+
75
+ for row in csv_reader: # Skip the header line
76
+ orcid = row["orcid"]
77
+
78
+ # Lambda file is ordered by last modified date
79
+ last_modified_str = row["last_modified"]
80
+ try:
81
+ last_modified_date = arrow.get(last_modified_str, date_format)
82
+ except arrow.parser.ParserError:
83
+ last_modified_date = arrow.get(last_modified_str, date_format_no_millis)
84
+
85
+ if last_modified_date < last_sync:
86
+ break
87
+ yield orcid
88
+
89
+ def _iter(self, orcids):
90
+ """Iterates over the ORCiD records yielding each one."""
91
+ with ThreadPoolExecutor(
92
+ max_workers=current_app.config["VOCABULARIES_ORCID_SYNC_MAX_WORKERS"]
93
+ ) as executor:
94
+ futures = [
95
+ executor.submit(
96
+ self._fetch_orcid_data,
97
+ orcid,
98
+ current_app.config["VOCABULARIES_ORCID_SUMMARIES_BUCKET"],
99
+ )
100
+ for orcid in orcids
101
+ ]
102
+ for future in as_completed(futures):
103
+ result = future.result()
104
+ if result is not None:
105
+ yield result
106
+
107
+ def read(self, item=None, *args, **kwargs):
108
+ """Streams the ORCiD lambda file, process it to get the ORCiDS to sync and yields it's data."""
109
+ # Read the file from S3
110
+ tar_content = self.s3_client.read_file(
111
+ "s3://orcid-lambda-file/last_modified.csv.tar"
112
+ )
113
+
114
+ orcids_to_sync = []
115
+ # Opens tar file and process it
116
+ with tarfile.open(fileobj=io.BytesIO(tar_content)) as tar:
117
+ # Iterate over each member (file or directory) in the tar file
118
+ for member in tar.getmembers():
119
+ # Extract the file
120
+ extracted_file = tar.extractfile(member)
121
+ if extracted_file:
122
+ # Process the file and get the ORCiDs to sync
123
+ orcids_to_sync.extend(self._process_lambda_file(extracted_file))
124
+
125
+ yield from self._iter(orcids_to_sync)
126
+
127
+
20
128
  class OrcidHTTPReader(SimpleHTTPReader):
21
129
  """ORCiD HTTP Reader."""
22
130
 
@@ -30,9 +138,26 @@ class OrcidHTTPReader(SimpleHTTPReader):
30
138
  super().__init__(origin, *args, **kwargs)
31
139
 
32
140
 
141
+ DEFAULT_NAMES_EXCLUDE_REGEX = r"[\p{P}\p{S}\p{Nd}\p{No}\p{Emoji}--,.()\-']"
142
+ """Regex to filter out names with punctuations, symbols, decimal numbers and emojis."""
143
+
144
+
33
145
  class OrcidTransformer(BaseTransformer):
34
146
  """Transforms an ORCiD record into a names record."""
35
147
 
148
+ def __init__(
149
+ self, *args, names_exclude_regex=DEFAULT_NAMES_EXCLUDE_REGEX, **kwargs
150
+ ) -> None:
151
+ """Constructor."""
152
+ self._names_exclude_regex = names_exclude_regex
153
+ super().__init__()
154
+
155
+ def _is_valid_name(self, name):
156
+ """Check whether the name passes the regex."""
157
+ if not self._names_exclude_regex:
158
+ return True
159
+ return not bool(re.search(self._names_exclude_regex, name, re.UNICODE | re.V1))
160
+
36
161
  def apply(self, stream_entry, **kwargs):
37
162
  """Applies the transformation to the stream entry."""
38
163
  record = stream_entry.entry
@@ -42,6 +167,11 @@ class OrcidTransformer(BaseTransformer):
42
167
  name = person.get("name")
43
168
  if name is None:
44
169
  raise TransformerError(f"Name not found in ORCiD entry.")
170
+ if name.get("family-name") is None:
171
+ raise TransformerError(f"Family name not found in ORCiD entry.")
172
+
173
+ if not self._is_valid_name(name["given-names"] + name["family-name"]):
174
+ raise TransformerError(f"Invalid characters in name.")
45
175
 
46
176
  entry = {
47
177
  "id": orcid_id,
@@ -89,6 +219,7 @@ class NamesServiceWriter(ServiceWriter):
89
219
 
90
220
  VOCABULARIES_DATASTREAM_READERS = {
91
221
  "orcid-http": OrcidHTTPReader,
222
+ "orcid-data-sync": OrcidDataSyncReader,
92
223
  }
93
224
 
94
225
 
@@ -128,3 +259,30 @@ DATASTREAM_CONFIG = {
128
259
 
129
260
  An origin is required for the reader.
130
261
  """
262
+
263
+ # TODO: Used on the jobs and should be set as a "PRESET" (naming to be defined)
264
+ ORCID_PRESET_DATASTREAM_CONFIG = {
265
+ "readers": [
266
+ {
267
+ "type": "orcid-data-sync",
268
+ },
269
+ {"type": "xml"},
270
+ ],
271
+ "transformers": [{"type": "orcid"}],
272
+ "writers": [
273
+ {
274
+ "type": "async",
275
+ "args": {
276
+ "writer": {
277
+ "type": "names-service",
278
+ }
279
+ },
280
+ }
281
+ ],
282
+ "batch_size": 1000,
283
+ "write_many": True,
284
+ }
285
+ """ORCiD Data Stream configuration.
286
+
287
+ An origin is required for the reader.
288
+ """
@@ -8,6 +8,7 @@
8
8
 
9
9
  """Vocabulary names."""
10
10
 
11
+ from flask_resources import JSONSerializer, ResponseHandler
11
12
  from invenio_db import db
12
13
  from invenio_records.dumpers import SearchDumper
13
14
  from invenio_records.dumpers.indexedat import IndexedAtDumperExt
@@ -18,6 +19,7 @@ from invenio_records_resources.records.systemfields import (
18
19
  ModelPIDField,
19
20
  PIDListRelation,
20
21
  )
22
+ from invenio_records_resources.resources.records.headers import etag_headers
21
23
 
22
24
  from ...services.permissions import PermissionPolicy
23
25
  from ..affiliations.api import Affiliation
@@ -63,4 +65,12 @@ record_type = RecordTypeFactory(
63
65
  permission_policy_cls=PermissionPolicy,
64
66
  # Resource layer
65
67
  endpoint_route="/names",
68
+ resource_cls_attrs={
69
+ "response_handlers": {
70
+ "application/json": ResponseHandler(JSONSerializer(), headers=etag_headers),
71
+ "application/vnd.inveniordm.v1+json": ResponseHandler(
72
+ JSONSerializer(), headers=etag_headers
73
+ ),
74
+ }
75
+ },
66
76
  )
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file is part of Invenio.
4
+ # Copyright (C) 2024 CERN.
5
+ #
6
+ # Invenio-Vocabularies is free software; you can redistribute it and/or
7
+ # modify it under the terms of the MIT License; see LICENSE file for more
8
+ # details.
9
+
10
+ """S3 client."""
11
+
12
+ from flask import current_app
13
+
14
+ try:
15
+ import s3fs
16
+ except ImportError:
17
+ s3fs = None
18
+
19
+
20
+ class S3Client:
21
+ """S3 client."""
22
+
23
+ def __init__(self, access_key, secret_key):
24
+ """Constructor."""
25
+ if s3fs is None:
26
+ raise Exception("s3fs is not installed.")
27
+
28
+ self.fs = s3fs.S3FileSystem(key=access_key, secret=secret_key)
29
+
30
+ def read_file(self, s3_path):
31
+ """Reads a file from S3."""
32
+ with self.fs.open(s3_path, "rb") as f:
33
+ return f.read()
34
+
35
+
36
+ class S3OrcidClient(S3Client):
37
+ """S3 ORCiD client."""
38
+
39
+ def __init__(self):
40
+ """Constructor."""
41
+ super().__init__(
42
+ access_key=current_app.config["VOCABULARIES_ORCID_ACCESS_KEY"],
43
+ secret_key=current_app.config["VOCABULARIES_ORCID_SECRET_KEY"],
44
+ )
@@ -9,9 +9,11 @@
9
9
 
10
10
  """Vocabulary subjects."""
11
11
 
12
+ from flask_resources import JSONSerializer, ResponseHandler
12
13
  from invenio_records.dumpers import SearchDumper
13
14
  from invenio_records.dumpers.indexedat import IndexedAtDumperExt
14
15
  from invenio_records_resources.factories.factory import RecordTypeFactory
16
+ from invenio_records_resources.resources.records.headers import etag_headers
15
17
 
16
18
  from ...records.pidprovider import PIDProviderFactory
17
19
  from ...records.systemfields import BaseVocabularyPIDFieldContext
@@ -42,4 +44,12 @@ record_type = RecordTypeFactory(
42
44
  permission_policy_cls=PermissionPolicy,
43
45
  # Resource layer
44
46
  endpoint_route="/subjects",
47
+ resource_cls_attrs={
48
+ "response_handlers": {
49
+ "application/json": ResponseHandler(JSONSerializer(), headers=etag_headers),
50
+ "application/vnd.inveniordm.v1+json": ResponseHandler(
51
+ JSONSerializer(), headers=etag_headers
52
+ ),
53
+ }
54
+ },
45
55
  )
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright (C) 2021-2022 CERN.
3
+ # Copyright (C) 2021-2024 CERN.
4
4
  #
5
5
  # Invenio-Vocabularies is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the MIT License; see LICENSE file for more
@@ -8,17 +8,41 @@
8
8
 
9
9
  """Base data stream."""
10
10
 
11
+ from flask import current_app
12
+
11
13
  from .errors import ReaderError, TransformerError, WriterError
12
14
 
13
15
 
14
16
  class StreamEntry:
15
17
  """Object to encapsulate streams processing."""
16
18
 
17
- def __init__(self, entry, errors=None):
18
- """Constructor."""
19
+ def __init__(self, entry, record=None, errors=None, op_type=None, exc=None):
20
+ """Constructor for the StreamEntry class.
21
+
22
+ :param entry (object): The entry object, usually a record dict.
23
+ :param record (object): The record object, usually a record class.
24
+ :param errors (list, optional): List of errors. Defaults to None.
25
+ :param op_type (str, optional): The operation type. Defaults to None.
26
+ :param exc (str, optional): The raised unhandled exception. Defaults to None.
27
+ """
19
28
  self.entry = entry
29
+ self.record = record
20
30
  self.filtered = False
21
31
  self.errors = errors or []
32
+ self.op_type = op_type
33
+ self.exc = exc
34
+
35
+ def log_errors(self, logger=None):
36
+ """Log the errors using the provided logger or the default logger.
37
+
38
+ :param logger (logging.Logger, optional): Logger instance to use. Defaults to None.
39
+ """
40
+ if logger is None:
41
+ logger = current_app.logger
42
+ for error in self.errors:
43
+ logger.error(f"Error in entry {self.entry}: {error}")
44
+ if self.exc:
45
+ logger.error(f"Exception in entry {self.entry}: {self.exc}")
22
46
 
23
47
 
24
48
  class DataStream:
@@ -39,15 +63,10 @@ class DataStream:
39
63
  """Checks if an stream_entry should be filtered out (skipped)."""
40
64
  return False
41
65
 
42
- def process(self, *args, **kwargs):
43
- """Iterates over the entries.
44
-
45
- Uses the reader to get the raw entries and transforms them.
46
- It will iterate over the `StreamEntry` objects returned by
47
- the reader, apply the transformations and yield the result of
48
- writing it.
49
- """
50
- for stream_entry in self.read():
66
+ def process_batch(self, batch, write_many=False):
67
+ """Process a batch of entries."""
68
+ transformed_entries = []
69
+ for stream_entry in batch:
51
70
  if stream_entry.errors:
52
71
  yield stream_entry # reading errors
53
72
  else:
@@ -58,7 +77,31 @@ class DataStream:
58
77
  transformed_entry.filtered = True
59
78
  yield transformed_entry
60
79
  else:
61
- yield self.write(transformed_entry)
80
+ transformed_entries.append(transformed_entry)
81
+ if transformed_entries:
82
+ if write_many:
83
+ yield from self.batch_write(transformed_entries)
84
+ else:
85
+ yield from (self.write(entry) for entry in transformed_entries)
86
+
87
+ def process(self, batch_size=100, write_many=False, *args, **kwargs):
88
+ """Iterates over the entries.
89
+
90
+ Uses the reader to get the raw entries and transforms them.
91
+ It will iterate over the `StreamEntry` objects returned by
92
+ the reader, apply the transformations and yield the result of
93
+ writing it.
94
+ """
95
+ batch = []
96
+ for stream_entry in self.read():
97
+ batch.append(stream_entry)
98
+ if len(batch) >= batch_size:
99
+ yield from self.process_batch(batch, write_many=write_many)
100
+ batch = []
101
+
102
+ # Process any remaining entries in the last batch
103
+ if batch:
104
+ yield from self.process_batch(batch, write_many=write_many)
62
105
 
63
106
  def read(self):
64
107
  """Recursively read the entries."""
@@ -107,6 +150,11 @@ class DataStream:
107
150
 
108
151
  return stream_entry
109
152
 
153
+ def batch_write(self, stream_entries, *args, **kwargs):
154
+ """Apply the transformations to an stream_entry. Errors are handler in the service layer."""
155
+ for writer in self._writers:
156
+ yield from writer.write_many(stream_entries)
157
+
110
158
  def total(self, *args, **kwargs):
111
159
  """The total of entries obtained from the origin."""
112
160
  raise NotImplementedError()
@@ -21,6 +21,7 @@ from json.decoder import JSONDecodeError
21
21
  import requests
22
22
  import yaml
23
23
  from lxml import etree
24
+ from lxml.html import fromstring
24
25
  from lxml.html import parse as html_parse
25
26
 
26
27
  from .errors import ReaderError
@@ -226,8 +227,13 @@ class XMLReader(BaseReader):
226
227
  def _iter(self, fp, *args, **kwargs):
227
228
  """Read and parse an XML file to dict."""
228
229
  # NOTE: We parse HTML, to skip XML validation and strip XML namespaces
229
- xml_tree = html_parse(fp).getroot()
230
- record = etree_to_dict(xml_tree)["html"]["body"].get("record")
230
+ record = None
231
+ try:
232
+ xml_tree = fromstring(fp)
233
+ record = etree_to_dict(xml_tree).get("record")
234
+ except Exception as e:
235
+ xml_tree = html_parse(fp).getroot()
236
+ record = etree_to_dict(xml_tree)["html"]["body"].get("record")
231
237
 
232
238
  if not record:
233
239
  raise ReaderError(f"Record not found in XML entry.")
@@ -15,11 +15,23 @@ from ..datastreams.factories import WriterFactory
15
15
 
16
16
 
17
17
  @shared_task(ignore_result=True)
18
- def write_entry(writer, entry):
18
+ def write_entry(writer_config, entry):
19
19
  """Write an entry.
20
20
 
21
21
  :param writer: writer configuration as accepted by the WriterFactory.
22
22
  :param entry: dictionary, StreamEntry is not serializable.
23
23
  """
24
- writer = WriterFactory.create(config=writer)
24
+ writer = WriterFactory.create(config=writer_config)
25
25
  writer.write(StreamEntry(entry))
26
+
27
+
28
+ @shared_task(ignore_result=True)
29
+ def write_many_entry(writer_config, entries):
30
+ """Write many entries.
31
+
32
+ :param writer: writer configuration as accepted by the WriterFactory.
33
+ :param entry: lisf ot dictionaries, StreamEntry is not serializable.
34
+ """
35
+ writer = WriterFactory.create(config=writer_config)
36
+ stream_entries = [StreamEntry(entry) for entry in entries]
37
+ writer.write_many(stream_entries)
@@ -20,12 +20,17 @@ from marshmallow import ValidationError
20
20
 
21
21
  from .datastreams import StreamEntry
22
22
  from .errors import WriterError
23
- from .tasks import write_entry
23
+ from .tasks import write_entry, write_many_entry
24
24
 
25
25
 
26
26
  class BaseWriter(ABC):
27
27
  """Base writer."""
28
28
 
29
+ def __init__(self, *args, **kwargs):
30
+ """Base initialization logic."""
31
+ # Add any base initialization here if needed
32
+ pass
33
+
29
34
  @abstractmethod
30
35
  def write(self, stream_entry, *args, **kwargs):
31
36
  """Writes the input stream entry to the target output.
@@ -36,6 +41,16 @@ class BaseWriter(ABC):
36
41
  """
37
42
  pass
38
43
 
44
+ @abstractmethod
45
+ def write_many(self, stream_entries, *args, **kwargs):
46
+ """Writes the input streams entry to the target output.
47
+
48
+ :returns: A List of StreamEntry. The result of writing the entry.
49
+ Raises WriterException in case of errors.
50
+
51
+ """
52
+ pass
53
+
39
54
 
40
55
  class ServiceWriter(BaseWriter):
41
56
  """Writes the entries to an RDM instance using a Service object."""
@@ -86,6 +101,25 @@ class ServiceWriter(BaseWriter):
86
101
  # TODO: Check if we can get the error message easier
87
102
  raise WriterError([{"InvalidRelationValue": err.args[0]}])
88
103
 
104
+ def write_many(self, stream_entries, *args, **kwargs):
105
+ """Writes the input entries using a given service."""
106
+ entries = [entry.entry for entry in stream_entries]
107
+ entries_with_id = [(self._entry_id(entry), entry) for entry in entries]
108
+ results = self._service.create_or_update_many(self._identity, entries_with_id)
109
+ stream_entries_processed = []
110
+ for entry, result in zip(entries, results):
111
+ processed_stream_entry = StreamEntry(
112
+ entry=entry,
113
+ record=result.record,
114
+ errors=result.errors,
115
+ op_type=result.op_type,
116
+ exc=result.exc,
117
+ )
118
+ processed_stream_entry.log_errors()
119
+ stream_entries_processed.append(processed_stream_entry)
120
+
121
+ return stream_entries_processed
122
+
89
123
 
90
124
  class YamlWriter(BaseWriter):
91
125
  """Writes the entries to a YAML file."""
@@ -108,6 +142,15 @@ class YamlWriter(BaseWriter):
108
142
 
109
143
  return stream_entry
110
144
 
145
+ def write_many(self, stream_entries, *args, **kwargs):
146
+ """Writes the yaml input entries."""
147
+ with open(self._filepath, "a") as file:
148
+ yaml.safe_dump(
149
+ [stream_entry.entry for stream_entry in stream_entries],
150
+ file,
151
+ allow_unicode=True,
152
+ )
153
+
111
154
 
112
155
  class AsyncWriter(BaseWriter):
113
156
  """Writes the entries asynchronously (celery task)."""
@@ -117,11 +160,19 @@ class AsyncWriter(BaseWriter):
117
160
 
118
161
  :param writer: writer to use.
119
162
  """
120
- self._writer = writer
121
163
  super().__init__(*args, **kwargs)
164
+ self._writer = writer
122
165
 
123
166
  def write(self, stream_entry, *args, **kwargs):
124
167
  """Launches a celery task to write an entry."""
125
168
  write_entry.delay(self._writer, stream_entry.entry)
126
169
 
127
170
  return stream_entry
171
+
172
+ def write_many(self, stream_entries, *args, **kwargs):
173
+ """Launches a celery task to write an entry."""
174
+ write_many_entry.delay(
175
+ self._writer, [stream_entry.entry for stream_entry in stream_entries]
176
+ )
177
+
178
+ return stream_entries
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright (C) 2024-2024 CERN.
3
+ # Copyright (C) 2024 CERN.
4
4
  #
5
5
  # Invenio-RDM-Records is free software; you can redistribute it and/or modify
6
6
  # it under the terms of the MIT License; see LICENSE file for more details.
@@ -1,5 +1,5 @@
1
1
  {#
2
- Copyright (C) 2024-2024 CERN.
2
+ Copyright (C) 2024 CERN.
3
3
 
4
4
  Invenio RDM Records is free software; you can redistribute it and/or modify
5
5
  it under the terms of the MIT License; see LICENSE file for more details.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: invenio-vocabularies
3
- Version: 4.2.0
3
+ Version: 4.4.0
4
4
  Summary: "Invenio module for managing vocabularies."
5
5
  Home-page: https://github.com/inveniosoftware/invenio-vocabularies
6
6
  Author: CERN
@@ -15,6 +15,7 @@ Requires-Dist: invenio-records-resources <7.0.0,>=6.0.0
15
15
  Requires-Dist: invenio-administration <3.0.0,>=2.0.0
16
16
  Requires-Dist: lxml >=4.5.0
17
17
  Requires-Dist: PyYAML >=5.4.1
18
+ Requires-Dist: regex >=2024.7.24
18
19
  Provides-Extra: elasticsearch7
19
20
  Requires-Dist: invenio-search[elasticsearch7] <3.0.0,>=2.1.0 ; extra == 'elasticsearch7'
20
21
  Provides-Extra: mysql
@@ -25,6 +26,8 @@ Requires-Dist: invenio-search[opensearch1] <3.0.0,>=2.1.0 ; extra == 'opensearch
25
26
  Provides-Extra: opensearch2
26
27
  Requires-Dist: invenio-search[opensearch2] <3.0.0,>=2.1.0 ; extra == 'opensearch2'
27
28
  Provides-Extra: postgresql
29
+ Provides-Extra: s3fs
30
+ Requires-Dist: s3fs >=2024.6.1 ; extra == 's3fs'
28
31
  Provides-Extra: sqlite
29
32
  Provides-Extra: tests
30
33
  Requires-Dist: pytest-black-ng >=0.4.0 ; extra == 'tests'
@@ -78,6 +81,16 @@ https://invenio-vocabularies.readthedocs.io/
78
81
  Changes
79
82
  =======
80
83
 
84
+ Version v4.4.0 (released 2024-08-09)
85
+
86
+ - services: use and adjust vnd.inveniordm.v1+json http accept header
87
+
88
+ Version v4.3.0 (released 2024-08-05)
89
+
90
+ - names: make names_exclude_regex configurable
91
+ - names: validate entry full names
92
+ - names: add orcid public data sync
93
+
81
94
  Version v4.2.0 (released 2024-07-24)
82
95
 
83
96
  - ror: check last update; use ld+json for metadata (#367)
@@ -1,6 +1,6 @@
1
- invenio_vocabularies/__init__.py,sha256=fOsOa6766lKWONOkvvx15eYhnKovuRiq9tWwrXh_5HI,377
2
- invenio_vocabularies/cli.py,sha256=0PpQttop3lTXxM29IpY70MXFFPhb9apbiutdb7WDBvw,5356
3
- invenio_vocabularies/config.py,sha256=f3J658yL7E0ShFN99Jq2q1b5AJF1lDeiDyzijDmh8qA,4497
1
+ invenio_vocabularies/__init__.py,sha256=scEcGFp9kWsXCw3wlDVG1nWWpWSVo6szcsMw-T-RcjY,377
2
+ invenio_vocabularies/cli.py,sha256=ToGc5dcGarDwNUgUXGqHIRT-3Uv1rleyFLKenwZdyBw,5719
3
+ invenio_vocabularies/config.py,sha256=hhlwenD5MDajCqmz2EmS7J5hVd2QPD2v5l_MjxM_56s,5011
4
4
  invenio_vocabularies/ext.py,sha256=GujJ4UARd4Fxf4z7zznRk9JAgHamZuYCOdrKU5czg00,5987
5
5
  invenio_vocabularies/factories.py,sha256=JBvvhg5f47Cy9cJZ1-OBalh750ErycwstLj1e9Gn4W8,2903
6
6
  invenio_vocabularies/fixtures.py,sha256=nNWwH04HFASjfj1oy5kMdcQGKmVjzUuA5wSw-ER1QAg,1585
@@ -43,10 +43,10 @@ invenio_vocabularies/assets/semantic-ui/translations/invenio_vocabularies/script
43
43
  invenio_vocabularies/assets/semantic-ui/translations/invenio_vocabularies/scripts/initCatalog.js,sha256=ghTi5iOIhT8AJoFGlxnBrHWiw2ts21dKwY_vWxmkiCE,682
44
44
  invenio_vocabularies/contrib/__init__.py,sha256=C5eDia6tAVBCrbb5hd_KnxmczyBoF87NIBUCLID-Tzc,240
45
45
  invenio_vocabularies/contrib/affiliations/__init__.py,sha256=rV8YAzBRoSKsBYcVjCNJh6j7ITuPRfurwj9HJHRjkN8,565
46
- invenio_vocabularies/contrib/affiliations/affiliations.py,sha256=Ph0z7bT_tudsWDKFaiksCBngsG-eO6Xpbe3KdejfLXI,1591
46
+ invenio_vocabularies/contrib/affiliations/affiliations.py,sha256=PCWBF9O7ba2yU3LftEt5pxJf14S-HymqnN4GqKhp-Mc,2027
47
47
  invenio_vocabularies/contrib/affiliations/api.py,sha256=5nIOvpfcseuAAg2XgblHc8jb7TAdfU79XOBRpL-p398,326
48
48
  invenio_vocabularies/contrib/affiliations/config.py,sha256=u1sHeihy0B0e6w5e3HsbrC9e8A4WTLKCYPXUdcX-ZTs,1605
49
- invenio_vocabularies/contrib/affiliations/datastreams.py,sha256=oojLIh9jCE8zr0dj8KFepxPqsOn2SBSXQsX8L3XwFxs,2383
49
+ invenio_vocabularies/contrib/affiliations/datastreams.py,sha256=Mn62EBBkGwO09qjrXY71qDIrS2mNgarb89ez69Vw4Yk,2384
50
50
  invenio_vocabularies/contrib/affiliations/facets.py,sha256=w316MGvtdyTpRCPOpCEmMxxLraRkbFFb1VvLkFlEc9o,1229
51
51
  invenio_vocabularies/contrib/affiliations/models.py,sha256=JUcj-1ydc2Cw2Rsc24JwXE3TFBJ_6fivhUYhGq4rT8A,329
52
52
  invenio_vocabularies/contrib/affiliations/resources.py,sha256=DBEbRxQmp-o-PeZlgFG588Q4sGcruuwIL8L9O-SzCes,435
@@ -86,7 +86,7 @@ invenio_vocabularies/contrib/common/ror/datastreams.py,sha256=joEiJwYawEaUggOZAQ
86
86
  invenio_vocabularies/contrib/funders/__init__.py,sha256=YxFXBDnT7NM8rFwxT_Ge3xXR2n17EM0alknQq7r_Bt8,478
87
87
  invenio_vocabularies/contrib/funders/api.py,sha256=QKGGeSnPHSoBfucvpaVruXT_txYidofZ080G3IxFkIo,306
88
88
  invenio_vocabularies/contrib/funders/config.py,sha256=BbzCRIcPxSwDllhjnGmdec5A126Zs1I4ZyVUCGi3jRA,1756
89
- invenio_vocabularies/contrib/funders/datastreams.py,sha256=G3UBT_kXei08zAKD44oaeERbkCVkJ9q9pFUoXpk3yiQ,2480
89
+ invenio_vocabularies/contrib/funders/datastreams.py,sha256=3tOwcN1mK5AVqtdU6KdguouGSUurPTO-FDWTKzK1eRo,2481
90
90
  invenio_vocabularies/contrib/funders/facets.py,sha256=a068TVtt74Ncu0latb177LFK8EdnpbMOWecAKozA04M,1245
91
91
  invenio_vocabularies/contrib/funders/funders.py,sha256=KtUydQzQZPKtT1YMbBSIRcGc2eBmdkk5Zqavo54zK1E,2298
92
92
  invenio_vocabularies/contrib/funders/models.py,sha256=RAU-_YVOUNVCn03_XGJ2czcVwXTaZPk5w7X_bMAgMOk,314
@@ -106,10 +106,11 @@ invenio_vocabularies/contrib/funders/mappings/v7/funders/funder-v1.0.0.json,sha2
106
106
  invenio_vocabularies/contrib/names/__init__.py,sha256=DBfsM7JMETZGaV5QmXEwE7zhCaAXvc2SZN6uXnW_V-c,451
107
107
  invenio_vocabularies/contrib/names/api.py,sha256=sEPn_jFX3gyoxgbdEUSIvOoPCUI8pocI6qCZO6mzCgQ,300
108
108
  invenio_vocabularies/contrib/names/config.py,sha256=hKDTEEBYGYOY6sMOArZjjkq2HJ6MJtRZp1geGLAFgRg,1735
109
- invenio_vocabularies/contrib/names/datastreams.py,sha256=iyXhm5dYOjjx5oTqkso1ymRnJWyjG3MsGb-j2Dfc5KY,3663
109
+ invenio_vocabularies/contrib/names/datastreams.py,sha256=PgCUwK0qpw_r0NTA17w7eqCDhgL2xd0tIXZ9-GjOT2M,9301
110
110
  invenio_vocabularies/contrib/names/models.py,sha256=SYdtDDG-y5Wq_d06YhiVO5n8gfxPW_mx-tECsIcv5H8,308
111
- invenio_vocabularies/contrib/names/names.py,sha256=fRNZW3yTmIE3FBB8zAcTdt1CXH6O8p6Ggz_7k9e8QTk,1987
111
+ invenio_vocabularies/contrib/names/names.py,sha256=NlCyc2n7vUSGN8vA5PmgkNk8GpSF9v7OjBSBhRSQjYI,2423
112
112
  invenio_vocabularies/contrib/names/resources.py,sha256=Z8XqLKfFKE69zdTTvcTDmpEZ6wqiqjIH5tp0LzXTSwQ,1588
113
+ invenio_vocabularies/contrib/names/s3client.py,sha256=c7B9_NbnXCfE4pE_yMTsT6uQ2hgbcRU-KY6nbWFuFzU,1063
113
114
  invenio_vocabularies/contrib/names/schema.py,sha256=eKhpNwBaACMEY0JWNrSUhr-40lXhkiHDRmM42KsLrYg,3354
114
115
  invenio_vocabularies/contrib/names/services.py,sha256=1viM-L8VEojmQWMisIOhDyl8KInPPCZEIf6tU8G07As,1763
115
116
  invenio_vocabularies/contrib/names/jsonschemas/__init__.py,sha256=pdDZdyoxqWbAQ6ngiclhYoDUsGKgRDRPXlIDy0U5Jzg,241
@@ -129,7 +130,7 @@ invenio_vocabularies/contrib/subjects/models.py,sha256=8XgbVRxDDvhWPjMWsoCriNlOK
129
130
  invenio_vocabularies/contrib/subjects/resources.py,sha256=0KRfUMizwgIziZybk4HnIjiSsXbrCv_XmguNPwnxoo8,506
130
131
  invenio_vocabularies/contrib/subjects/schema.py,sha256=jWPhpCDoScf8fRs7T15kcXnprWw31OuGBKJUziQ3t8A,1120
131
132
  invenio_vocabularies/contrib/subjects/services.py,sha256=s1U6HMmpjuz7rrgR0DtT9C28TC6sZEeDTsa4Jh1TXQk,864
132
- invenio_vocabularies/contrib/subjects/subjects.py,sha256=y5IidPbmYV3zs5aEp7CFzijYoVwWZ5GIMwl1kD0e0Eg,1441
133
+ invenio_vocabularies/contrib/subjects/subjects.py,sha256=NwZycExLyV8l7ikGStH4GOecVuDSxFT70KoNv6qC78I,1877
133
134
  invenio_vocabularies/contrib/subjects/jsonschemas/__init__.py,sha256=WowVUST1JoEDS3-xeHhCJvIgC9nzMkFs8XRks9zgzaM,292
134
135
  invenio_vocabularies/contrib/subjects/jsonschemas/subjects/subject-v1.0.0.json,sha256=jlKAY_0oZH2zMlK8F4oNI2Xbq0ENYhHrM_w5nM9d0Fc,914
135
136
  invenio_vocabularies/contrib/subjects/mappings/__init__.py,sha256=Qk-yj1ENsTmijO8ImWuDYGzXi6QQ2VjP4DbjrpRfDk8,243
@@ -140,13 +141,13 @@ invenio_vocabularies/contrib/subjects/mappings/os-v2/subjects/subject-v1.0.0.jso
140
141
  invenio_vocabularies/contrib/subjects/mappings/v7/__init__.py,sha256=QK__a1749g2UN3fBqOr9jx8ccZHWAuvd6DSN4B1jJW4,258
141
142
  invenio_vocabularies/contrib/subjects/mappings/v7/subjects/subject-v1.0.0.json,sha256=Gr2dnAiqKgOISW2zhv4RZsssIGQPBNtPpgSu_ysHGDs,1166
142
143
  invenio_vocabularies/datastreams/__init__.py,sha256=VPefh6k4Q3eYxKIW8I5zXUGucntp7VHxaOR5Vhgkfmg,412
143
- invenio_vocabularies/datastreams/datastreams.py,sha256=_jSXv2yAvSjt8btMoLJlqXOkqBzYb3Xe9m2GH50Nwag,3987
144
+ invenio_vocabularies/datastreams/datastreams.py,sha256=SpI6ivmf2LIDS2JSkxoM2v5kRmrPoRDtAG5fuzZO4oQ,6078
144
145
  invenio_vocabularies/datastreams/errors.py,sha256=IDUZ3gNtYGrhcOgApHCms1gNNJTyJzoMPmG5JtIeYNU,678
145
146
  invenio_vocabularies/datastreams/factories.py,sha256=H8a2gAy7KNImtdCdtqpVKC5gIvE3ON6U1Wn1_zaMlQ4,2181
146
- invenio_vocabularies/datastreams/readers.py,sha256=9WypPQFGWEIKcrra6oC8yxdGzVG3utd3CtrGx-AUxLM,10834
147
- invenio_vocabularies/datastreams/tasks.py,sha256=V3bm8ebdaB1tkrZWR1DuCscxSzP-O5maZecPkOF3Dyw,694
147
+ invenio_vocabularies/datastreams/readers.py,sha256=JT76oKeD1N2u-0zWlyI7GZ5tR1aGc9IZe7pdTtNaSaM,11038
148
+ invenio_vocabularies/datastreams/tasks.py,sha256=0fuH_PRt9Ncv6WHM4pkYmfheRVGDKkERZiMPvgV4bZU,1129
148
149
  invenio_vocabularies/datastreams/transformers.py,sha256=wspny-kazYMRHjkkyFfRVNIYzJxbjAqokRCBQ_-gXcY,1357
149
- invenio_vocabularies/datastreams/writers.py,sha256=UkrPOKyAOu8SsNyxBgdxYelCtPcBab6kbaIw-swVxlw,4153
150
+ invenio_vocabularies/datastreams/writers.py,sha256=gFJ-NVbpIuR2aw4v5TyxR5roS-qVnETyqEVY1o8rYKk,6059
150
151
  invenio_vocabularies/datastreams/xml.py,sha256=HFa-lfxj7kFrr2IjeN1jxSLDfcvpBwO9nZLZF2-BryE,997
151
152
  invenio_vocabularies/records/__init__.py,sha256=Uj7O6fYdAtLOkLXUGSAYPADBB7aqP4yVs9b6OAjA158,243
152
153
  invenio_vocabularies/records/api.py,sha256=Lynt6Sz4BVN1orh0zgJ5ljhnUobEtcq8c22PmSeUo2U,1494
@@ -181,9 +182,9 @@ invenio_vocabularies/services/schema.py,sha256=mwIBFylpQlWw1M6h_axc-z4Yd7X3Z1S0P
181
182
  invenio_vocabularies/services/service.py,sha256=rUPeb86Jv2WXzRp-RZDHLWsouCmJcqHpCmgrpLm2Vs4,6387
182
183
  invenio_vocabularies/services/tasks.py,sha256=AH0XifkOypsEdh8LyjmlHnPLQK5qqUJC8cNVWGkbqks,788
183
184
  invenio_vocabularies/services/custom_fields/__init__.py,sha256=QgvSsn-S1xLzbZ57pjjGTt5oI3HqzXHVjwGTtuPgzN8,421
184
- invenio_vocabularies/services/custom_fields/subject.py,sha256=TrtmRgk3Vj_NxmDhC_7sM8MAIje4AGXBxYIk7nIgt0o,2236
185
+ invenio_vocabularies/services/custom_fields/subject.py,sha256=ZM-ZkaxoouF9lL62smOtLxsjQQZwiQs0jG3qGruP6nY,2231
185
186
  invenio_vocabularies/services/custom_fields/vocabulary.py,sha256=oQwI8Aoi2Nr9k3eWKnde5H7RXc7qdlATSeI6coy8UR0,3020
186
- invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/subjects.html,sha256=h9syHkwZ6ltgjWw_0M5UoR6h6Re2imJ7uQ6M6peOyhk,686
187
+ invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/subjects.html,sha256=Fr8xRfKYiytuTfbtH7gfasNXwFIcjPFnXV4F5oGNUkM,681
187
188
  invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabularies-list.html,sha256=-gDwRctqIkSzh9ial8zfbA4o41ARM-Mq-THkcJ87U00,359
188
189
  invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabulary-details.html,sha256=2dfQzRFl5RwUwle245sxWGnObwJQXr-e_bBzpe_PkkA,2684
189
190
  invenio_vocabularies/translations/messages.pot,sha256=IzTTWdWknzmKKtl1UNUxPOwHjfBB_iidm_eeEY2kV-M,3907
@@ -281,10 +282,10 @@ invenio_vocabularies/translations/zh_CN/LC_MESSAGES/messages.mo,sha256=g1I5aNO8r
281
282
  invenio_vocabularies/translations/zh_CN/LC_MESSAGES/messages.po,sha256=vg8qC8ofpAdJ3mQz7mWM1ylKDpiNWXFs7rlMdSPkgKk,4629
282
283
  invenio_vocabularies/translations/zh_TW/LC_MESSAGES/messages.mo,sha256=cqSm8NtMAwrP9O6qbmtkDtRT1e9D93qpsJN5X9_PPVw,600
283
284
  invenio_vocabularies/translations/zh_TW/LC_MESSAGES/messages.po,sha256=9ACePz_EpB-LfcIJajZ2kp8Q04tcdrQLOtug162ZUss,4115
284
- invenio_vocabularies-4.2.0.dist-info/AUTHORS.rst,sha256=8d0p_WWE1r9DavvzMDi2D4YIGBHiMYcN3LYxqQOj8sY,291
285
- invenio_vocabularies-4.2.0.dist-info/LICENSE,sha256=UvI8pR8jGWqe0sTkb_hRG6eIrozzWwWzyCGEpuXX4KE,1062
286
- invenio_vocabularies-4.2.0.dist-info/METADATA,sha256=2pdaEFZ5Z35PmaNmdeuq-TDu0P1X8KCeyUhZh6NvW-0,8167
287
- invenio_vocabularies-4.2.0.dist-info/WHEEL,sha256=-G_t0oGuE7UD0DrSpVZnq1hHMBV9DD2XkS5v7XpmTnk,110
288
- invenio_vocabularies-4.2.0.dist-info/entry_points.txt,sha256=qHHFkyU3r0COsKm5gCYuhP8tfsioBggxKAiEXNAbbjM,2803
289
- invenio_vocabularies-4.2.0.dist-info/top_level.txt,sha256=x1gRNbaODF_bCD0SBLM3nVOFPGi06cmGX5X94WKrFKk,21
290
- invenio_vocabularies-4.2.0.dist-info/RECORD,,
285
+ invenio_vocabularies-4.4.0.dist-info/AUTHORS.rst,sha256=8d0p_WWE1r9DavvzMDi2D4YIGBHiMYcN3LYxqQOj8sY,291
286
+ invenio_vocabularies-4.4.0.dist-info/LICENSE,sha256=UvI8pR8jGWqe0sTkb_hRG6eIrozzWwWzyCGEpuXX4KE,1062
287
+ invenio_vocabularies-4.4.0.dist-info/METADATA,sha256=hYz0Gy3C37wuawLdd36nJ9_RZiS130E4bhKN8tlZa9E,8535
288
+ invenio_vocabularies-4.4.0.dist-info/WHEEL,sha256=-G_t0oGuE7UD0DrSpVZnq1hHMBV9DD2XkS5v7XpmTnk,110
289
+ invenio_vocabularies-4.4.0.dist-info/entry_points.txt,sha256=qHHFkyU3r0COsKm5gCYuhP8tfsioBggxKAiEXNAbbjM,2803
290
+ invenio_vocabularies-4.4.0.dist-info/top_level.txt,sha256=x1gRNbaODF_bCD0SBLM3nVOFPGi06cmGX5X94WKrFKk,21
291
+ invenio_vocabularies-4.4.0.dist-info/RECORD,,