invenio-vocabularies 5.0.3__py2.py3-none-any.whl → 6.0.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 (40) hide show
  1. invenio_vocabularies/__init__.py +1 -1
  2. invenio_vocabularies/cli.py +7 -2
  3. invenio_vocabularies/config.py +13 -0
  4. invenio_vocabularies/contrib/affiliations/datastreams.py +95 -1
  5. invenio_vocabularies/contrib/awards/awards.py +15 -4
  6. invenio_vocabularies/contrib/awards/datastreams.py +156 -60
  7. invenio_vocabularies/contrib/awards/jsonschemas/awards/award-v1.0.0.json +55 -0
  8. invenio_vocabularies/contrib/awards/mappings/os-v1/awards/award-v1.0.0.json +44 -1
  9. invenio_vocabularies/contrib/awards/mappings/os-v2/awards/award-v1.0.0.json +44 -1
  10. invenio_vocabularies/contrib/awards/mappings/v7/awards/award-v1.0.0.json +44 -1
  11. invenio_vocabularies/contrib/awards/schema.py +16 -1
  12. invenio_vocabularies/contrib/awards/serializer.py +8 -1
  13. invenio_vocabularies/contrib/common/openaire/__init__.py +9 -0
  14. invenio_vocabularies/contrib/common/openaire/datastreams.py +84 -0
  15. invenio_vocabularies/contrib/funders/config.py +8 -3
  16. invenio_vocabularies/contrib/funders/mappings/os-v1/funders/funder-v2.0.0.json +17 -1
  17. invenio_vocabularies/contrib/funders/mappings/os-v2/funders/funder-v2.0.0.json +17 -1
  18. invenio_vocabularies/contrib/names/datastreams.py +12 -2
  19. invenio_vocabularies/contrib/names/names.py +4 -3
  20. invenio_vocabularies/contrib/names/permissions.py +20 -0
  21. invenio_vocabularies/contrib/subjects/datastreams.py +12 -6
  22. invenio_vocabularies/contrib/subjects/euroscivoc/__init__.py +9 -0
  23. invenio_vocabularies/contrib/subjects/euroscivoc/datastreams.py +171 -0
  24. invenio_vocabularies/contrib/subjects/jsonschemas/subjects/subject-v1.0.0.json +16 -0
  25. invenio_vocabularies/contrib/subjects/mappings/os-v1/subjects/subject-v1.0.0.json +14 -0
  26. invenio_vocabularies/contrib/subjects/mappings/os-v2/subjects/subject-v1.0.0.json +14 -0
  27. invenio_vocabularies/contrib/subjects/mappings/v7/subjects/subject-v1.0.0.json +14 -0
  28. invenio_vocabularies/contrib/subjects/mesh/__init__.py +9 -0
  29. invenio_vocabularies/contrib/subjects/schema.py +30 -6
  30. invenio_vocabularies/datastreams/readers.py +15 -4
  31. invenio_vocabularies/datastreams/transformers.py +15 -4
  32. invenio_vocabularies/datastreams/writers.py +44 -12
  33. invenio_vocabularies/factories.py +30 -0
  34. {invenio_vocabularies-5.0.3.dist-info → invenio_vocabularies-6.0.0.dist-info}/METADATA +16 -1
  35. {invenio_vocabularies-5.0.3.dist-info → invenio_vocabularies-6.0.0.dist-info}/RECORD +40 -34
  36. {invenio_vocabularies-5.0.3.dist-info → invenio_vocabularies-6.0.0.dist-info}/AUTHORS.rst +0 -0
  37. {invenio_vocabularies-5.0.3.dist-info → invenio_vocabularies-6.0.0.dist-info}/LICENSE +0 -0
  38. {invenio_vocabularies-5.0.3.dist-info → invenio_vocabularies-6.0.0.dist-info}/WHEEL +0 -0
  39. {invenio_vocabularies-5.0.3.dist-info → invenio_vocabularies-6.0.0.dist-info}/entry_points.txt +0 -0
  40. {invenio_vocabularies-5.0.3.dist-info → invenio_vocabularies-6.0.0.dist-info}/top_level.txt +0 -0
@@ -10,6 +10,6 @@
10
10
 
11
11
  from .ext import InvenioVocabularies
12
12
 
13
- __version__ = "5.0.3"
13
+ __version__ = "6.0.0"
14
14
 
15
15
  __all__ = ("__version__", "InvenioVocabularies")
@@ -101,9 +101,14 @@ def update(vocabulary, filepath=None, origin=None):
101
101
 
102
102
  for w_conf in config["writers"]:
103
103
  if w_conf["type"] == "async":
104
- w_conf["args"]["writer"]["args"]["update"] = True
104
+ w_conf_update = w_conf["args"]["writer"]
105
105
  else:
106
- w_conf["args"]["update"] = True
106
+ w_conf_update = w_conf
107
+
108
+ if "args" in w_conf_update:
109
+ w_conf_update["args"]["update"] = True
110
+ else:
111
+ w_conf_update["args"] = {"update": True}
107
112
 
108
113
  success, errored, filtered = _process_vocab(config)
109
114
 
@@ -43,8 +43,17 @@ VOCABULARIES_IDENTIFIER_SCHEMES = {
43
43
  }
44
44
  """"Generic identifier schemes, usable by other vocabularies."""
45
45
 
46
+
47
+ def is_pic(val):
48
+ """Test if argument is a Participant Identification Code (PIC)."""
49
+ if len(val) != 9:
50
+ return False
51
+ return val.isdigit()
52
+
53
+
46
54
  VOCABULARIES_AFFILIATION_SCHEMES = {
47
55
  **VOCABULARIES_IDENTIFIER_SCHEMES,
56
+ "pic": {"label": _("PIC"), "validator": is_pic},
48
57
  }
49
58
  """Affiliations allowed identifier schemes."""
50
59
 
@@ -107,6 +116,7 @@ VOCABULARIES_NAMES_SCHEMES = {
107
116
 
108
117
  VOCABULARIES_SUBJECTS_SCHEMES = {
109
118
  "gnd": {"label": _("GND"), "validator": idutils.is_gnd, "datacite": "GND"},
119
+ "url": {"label": _("URL"), "validator": idutils.is_url},
110
120
  }
111
121
  """Subjects allowed identifier schemes."""
112
122
 
@@ -162,6 +172,9 @@ VOCABULARIES_TYPES_SEARCH = {
162
172
  }
163
173
  """Vocabulary type search configuration."""
164
174
 
175
+ SUBJECTS_EUROSCIVOC_FILE_URL = "https://op.europa.eu/o/opportal-service/euvoc-download-handler?cellarURI=http%3A%2F%2Fpublications.europa.eu%2Fresource%2Fdistribution%2Feuroscivoc%2F20231115-0%2Frdf%2Fskos_ap_eu%2FEuroSciVoc-skos-ap-eu.rdf&fileName=EuroSciVoc-skos-ap-eu.rdf"
176
+ """Subject EuroSciVoc file download link."""
177
+
165
178
  VOCABULARIES_ORCID_ACCESS_KEY = "TODO"
166
179
  """ORCID access key to access the s3 bucket."""
167
180
  VOCABULARIES_ORCID_SECRET_KEY = "TODO"
@@ -10,8 +10,9 @@
10
10
  """Affiliations datastreams, transformers, writers and readers."""
11
11
 
12
12
  from flask import current_app
13
- from invenio_i18n import lazy_gettext as _
14
13
 
14
+ from ...datastreams.errors import TransformerError, WriterError
15
+ from ...datastreams.transformers import BaseTransformer
15
16
  from ...datastreams.writers import ServiceWriter
16
17
  from ..common.ror.datastreams import RORTransformer
17
18
 
@@ -46,16 +47,78 @@ class AffiliationsRORTransformer(RORTransformer):
46
47
  )
47
48
 
48
49
 
50
+ class OpenAIREOrganizationTransformer(BaseTransformer):
51
+ """OpenAIRE Organization Transformer."""
52
+
53
+ def apply(self, stream_entry, **kwargs):
54
+ """Applies the transformation to the stream entry."""
55
+ record = stream_entry.entry
56
+
57
+ if "id" not in record:
58
+ raise TransformerError([f"No id for: {record}"])
59
+
60
+ if not record["id"].startswith("openorgs____::"):
61
+ raise TransformerError([f"Not valid OpenAIRE OpenOrgs id for: {record}"])
62
+
63
+ if "pid" not in record:
64
+ raise TransformerError([f"No alternative identifiers for: {record}"])
65
+
66
+ organization = {}
67
+
68
+ for pid in record["pid"]:
69
+ if pid["scheme"] == "ROR":
70
+ organization["id"] = pid["value"].removeprefix("https://ror.org/")
71
+ elif pid["scheme"] == "PIC":
72
+ organization["identifiers"] = [
73
+ {
74
+ "scheme": "pic",
75
+ "identifier": pid["value"],
76
+ }
77
+ ]
78
+
79
+ stream_entry.entry = organization
80
+ return stream_entry
81
+
82
+
83
+ class OpenAIREAffiliationsServiceWriter(ServiceWriter):
84
+ """OpenAIRE Affiliations service writer."""
85
+
86
+ def __init__(self, *args, **kwargs):
87
+ """Constructor."""
88
+ service_or_name = kwargs.pop("service_or_name", "affiliations")
89
+ # Here we only update and we do not insert, since OpenAIRE data is used to augment existing affiliations
90
+ # (with PIC identifiers) and is not used to create new affiliations.
91
+ super().__init__(
92
+ service_or_name=service_or_name, insert=False, update=True, *args, **kwargs
93
+ )
94
+
95
+ def _entry_id(self, entry):
96
+ """Get the id from an entry."""
97
+ return entry["id"]
98
+
99
+ def write(self, stream_entry, *args, **kwargs):
100
+ """Writes the input entry using a given service."""
101
+ entry = stream_entry.entry
102
+
103
+ return super().write(stream_entry, *args, **kwargs)
104
+
105
+ def write_many(self, stream_entries, *args, **kwargs):
106
+ """Writes the input entries using a given service."""
107
+ return super().write_many(stream_entries, *args, **kwargs)
108
+
109
+
49
110
  VOCABULARIES_DATASTREAM_READERS = {}
50
111
  """Affiliations datastream readers."""
51
112
 
52
113
  VOCABULARIES_DATASTREAM_WRITERS = {
53
114
  "affiliations-service": AffiliationsServiceWriter,
115
+ "openaire-affiliations-service": OpenAIREAffiliationsServiceWriter,
54
116
  }
55
117
  """Affiliations datastream writers."""
56
118
 
57
119
  VOCABULARIES_DATASTREAM_TRANSFORMERS = {
58
120
  "ror-affiliations": AffiliationsRORTransformer,
121
+ "openaire-organization": OpenAIREOrganizationTransformer,
59
122
  }
60
123
  """Affiliations datastream transformers."""
61
124
 
@@ -90,3 +153,34 @@ DATASTREAM_CONFIG = {
90
153
 
91
154
  An origin is required for the reader.
92
155
  """
156
+
157
+ DATASTREAM_CONFIG_OPENAIRE = {
158
+ "readers": [
159
+ {"type": "openaire-http", "args": {"tar_href": "/organization.tar"}},
160
+ {
161
+ "type": "tar",
162
+ "args": {
163
+ "regex": "\\.json.gz$",
164
+ "mode": "r",
165
+ },
166
+ },
167
+ {"type": "gzip"},
168
+ {"type": "jsonl"},
169
+ ],
170
+ "transformers": [
171
+ {
172
+ "type": "openaire-organization",
173
+ },
174
+ ],
175
+ "writers": [
176
+ {
177
+ "type": "async",
178
+ "args": {
179
+ "writer": {
180
+ "type": "openaire-affiliations-service",
181
+ }
182
+ },
183
+ }
184
+ ],
185
+ }
186
+ """Alternative Data Stream configuration for OpenAIRE Affiliations."""
@@ -18,24 +18,35 @@ from invenio_db import db
18
18
  from invenio_records.dumpers import SearchDumper
19
19
  from invenio_records.dumpers.indexedat import IndexedAtDumperExt
20
20
  from invenio_records.dumpers.relations import RelationDumperExt
21
- from invenio_records.systemfields import RelationsField
21
+ from invenio_records.systemfields import MultiRelationsField
22
22
  from invenio_records_resources.factories.factory import RecordTypeFactory
23
- from invenio_records_resources.records.systemfields import ModelPIDField, PIDRelation
23
+ from invenio_records_resources.records.systemfields import (
24
+ ModelPIDField,
25
+ PIDListRelation,
26
+ PIDRelation,
27
+ )
24
28
  from invenio_records_resources.resources.records.headers import etag_headers
25
29
 
26
30
  from ...services.permissions import PermissionPolicy
27
31
  from ..funders.api import Funder
32
+ from ..subjects.api import Subject
28
33
  from .config import AwardsSearchOptions, service_components
29
34
  from .schema import AwardSchema
30
35
  from .serializer import AwardL10NItemSchema
31
36
 
32
- award_relations = RelationsField(
37
+ award_relations = MultiRelationsField(
33
38
  funders=PIDRelation(
34
39
  "funder",
35
40
  keys=["name"],
36
41
  pid_field=Funder.pid,
37
42
  cache_key="funder",
38
- )
43
+ ),
44
+ subjects=PIDListRelation(
45
+ "subjects",
46
+ keys=["subject", "scheme", "identifiers", "props"],
47
+ pid_field=Subject.pid,
48
+ cache_key="subjects",
49
+ ),
39
50
  )
40
51
 
41
52
  record_type = RecordTypeFactory(
@@ -11,70 +11,19 @@
11
11
  import io
12
12
 
13
13
  import requests
14
+ from flask import current_app
14
15
  from invenio_access.permissions import system_identity
15
16
  from invenio_i18n import lazy_gettext as _
16
17
 
17
- from ...datastreams.errors import ReaderError, TransformerError
18
+ from invenio_vocabularies.datastreams.errors import ReaderError
19
+
20
+ from ...datastreams.errors import TransformerError
18
21
  from ...datastreams.readers import BaseReader
19
22
  from ...datastreams.transformers import BaseTransformer
20
23
  from ...datastreams.writers import ServiceWriter
21
24
  from .config import awards_ec_ror_id, awards_openaire_funders_mapping
22
25
 
23
26
 
24
- class OpenAIREProjectHTTPReader(BaseReader):
25
- """OpenAIRE Project HTTP Reader returning an in-memory binary stream of the latest OpenAIRE Graph Dataset project tar file."""
26
-
27
- def _iter(self, fp, *args, **kwargs):
28
- raise NotImplementedError(
29
- "OpenAIREProjectHTTPReader downloads one file and therefore does not iterate through items"
30
- )
31
-
32
- def read(self, item=None, *args, **kwargs):
33
- """Reads the latest OpenAIRE Graph Dataset project tar file from Zenodo and yields an in-memory binary stream of it."""
34
- if item:
35
- raise NotImplementedError(
36
- "OpenAIREProjectHTTPReader does not support being chained after another reader"
37
- )
38
-
39
- if self._origin == "full":
40
- # OpenAIRE Graph Dataset
41
- api_url = "https://zenodo.org/api/records/3516917"
42
- elif self._origin == "diff":
43
- # OpenAIRE Graph dataset: new collected projects
44
- api_url = "https://zenodo.org/api/records/6419021"
45
- else:
46
- raise ReaderError("The --origin option should be either 'full' or 'diff'")
47
-
48
- # Call the signposting `linkset+json` endpoint for the Concept DOI (i.e. latest version) of the OpenAIRE Graph Dataset.
49
- # See: https://github.com/inveniosoftware/rfcs/blob/master/rfcs/rdm-0071-signposting.md#provide-an-applicationlinksetjson-endpoint
50
- headers = {"Accept": "application/linkset+json"}
51
- api_resp = requests.get(api_url, headers=headers)
52
- api_resp.raise_for_status()
53
-
54
- # Extract the Landing page Link Set Object located as the first (index 0) item.
55
- landing_page_linkset = api_resp.json()["linkset"][0]
56
-
57
- # Extract the URL of the only project tar file linked to the record.
58
- landing_page_project_tar_items = [
59
- item
60
- for item in landing_page_linkset["item"]
61
- if item["type"] == "application/x-tar"
62
- and item["href"].endswith("/project.tar")
63
- ]
64
- if len(landing_page_project_tar_items) != 1:
65
- raise ReaderError(
66
- f"Expected 1 project tar item but got {len(landing_page_project_tar_items)}"
67
- )
68
- file_url = landing_page_project_tar_items[0]["href"]
69
-
70
- # Download the project tar file and fully load the response bytes content in memory.
71
- # The bytes content are then wrapped by a BytesIO to be file-like object (as required by `tarfile.open`).
72
- # Using directly `file_resp.raw` is not possible since `tarfile.open` requires the file-like object to be seekable.
73
- file_resp = requests.get(file_url)
74
- file_resp.raise_for_status()
75
- yield io.BytesIO(file_resp.content)
76
-
77
-
78
27
  class AwardsServiceWriter(ServiceWriter):
79
28
  """Funders service writer."""
80
29
 
@@ -125,10 +74,7 @@ class OpenAIREProjectTransformer(BaseTransformer):
125
74
 
126
75
  funding = next(iter(record.get("funding", [])), None)
127
76
  if funding:
128
- funding_stream_id = funding.get("funding_stream", {}).get("id", "")
129
- # Example funding stream ID: `EC::HE::HORIZON-AG-UN`. We need the `EC`
130
- # string, i.e. the second "part" of the identifier.
131
- program = next(iter(funding_stream_id.split("::")[1:2]), "")
77
+ program = funding.get("fundingStream", {}).get("id", "")
132
78
  if program:
133
79
  award["program"] = program
134
80
 
@@ -172,20 +118,170 @@ class OpenAIREProjectTransformer(BaseTransformer):
172
118
  return stream_entry
173
119
 
174
120
 
121
+ class CORDISProjectHTTPReader(BaseReader):
122
+ """CORDIS Project HTTP Reader returning an in-memory binary stream of the latest CORDIS Horizon Europe project zip file."""
123
+
124
+ def _iter(self, fp, *args, **kwargs):
125
+ raise NotImplementedError(
126
+ "CORDISProjectHTTPReader downloads one file and therefore does not iterate through items"
127
+ )
128
+
129
+ def read(self, item=None, *args, **kwargs):
130
+ """Reads the latest CORDIS Horizon Europe project zip file and yields an in-memory binary stream of it."""
131
+ if item:
132
+ raise NotImplementedError(
133
+ "CORDISProjectHTTPReader does not support being chained after another reader"
134
+ )
135
+
136
+ if self._origin == "HE":
137
+ file_url = "https://cordis.europa.eu/data/cordis-HORIZONprojects-xml.zip"
138
+ elif self._origin == "H2020":
139
+ file_url = "https://cordis.europa.eu/data/cordis-h2020projects-xml.zip"
140
+ elif self._origin == "FP7":
141
+ file_url = "https://cordis.europa.eu/data/cordis-fp7projects-xml.zip"
142
+ else:
143
+ raise ReaderError(
144
+ "The --origin option should be either 'HE' (for Horizon Europe) or 'H2020' (for Horizon 2020) or 'FP7'"
145
+ )
146
+
147
+ # Download the ZIP file and fully load the response bytes content in memory.
148
+ # The bytes content are then wrapped by a BytesIO to be file-like object (as required by `zipfile.ZipFile`).
149
+ # Using directly `file_resp.raw` is not possible since `zipfile.ZipFile` requires the file-like object to be seekable.
150
+ file_resp = requests.get(file_url)
151
+ file_resp.raise_for_status()
152
+ yield io.BytesIO(file_resp.content)
153
+
154
+
155
+ class CORDISProjectTransformer(BaseTransformer):
156
+ """Transforms a CORDIS project record into an award record."""
157
+
158
+ def apply(self, stream_entry, **kwargs):
159
+ """Applies the transformation to the stream entry."""
160
+ record = stream_entry.entry
161
+ award = {}
162
+
163
+ # Here `id` is the project ID, which will be used to attach the update to the existing project.
164
+ award["id"] = (
165
+ f"{current_app.config['VOCABULARIES_AWARDS_EC_ROR_ID']}::{record['id']}"
166
+ )
167
+
168
+ categories = record.get("relations", {}).get("categories", {}).get("category")
169
+ if categories:
170
+ if isinstance(categories, dict):
171
+ categories = [categories]
172
+
173
+ award["subjects"] = [
174
+ {"id": f"euroscivoc:{vocab_id}"}
175
+ for category in categories
176
+ if category.get("@classification") == "euroSciVoc"
177
+ and (vocab_id := category["code"].split("/")[-1]).isdigit()
178
+ ]
179
+
180
+ organizations = (
181
+ record.get("relations", {}).get("associations", {}).get("organization")
182
+ )
183
+ if organizations:
184
+ # Projects with a single organization are not wrapped in a list,
185
+ # so we do this here to be able to iterate over it.
186
+ organizations = (
187
+ organizations if isinstance(organizations, list) else [organizations]
188
+ )
189
+ award["organizations"] = []
190
+ for organization in organizations:
191
+ # Some organizations in FP7 projects do not have a "legalname" key,
192
+ # for instance the 14th participant in "SAGE" https://cordis.europa.eu/project/id/999902.
193
+ # In this case, fully skip the organization entry.
194
+ if "legalname" not in organization:
195
+ continue
196
+
197
+ organization_data = {
198
+ "organization": organization["legalname"],
199
+ }
200
+
201
+ # Some organizations in FP7 projects do not have an "id" key (the PIC identifier),
202
+ # for instance "AIlGreenVehicles" in "MOTORBRAIN" https://cordis.europa.eu/project/id/270693.
203
+ # In this case, still store the name but skip the identifier part.
204
+ if "id" in organization:
205
+ organization_data.update(
206
+ {
207
+ "scheme": "pic",
208
+ "id": organization["id"],
209
+ }
210
+ )
211
+
212
+ award["organizations"].append(organization_data)
213
+
214
+ stream_entry.entry = award
215
+ return stream_entry
216
+
217
+
218
+ class CORDISAwardsServiceWriter(ServiceWriter):
219
+ """CORDIS Awards service writer."""
220
+
221
+ def __init__(self, *args, **kwargs):
222
+ """Constructor."""
223
+ service_or_name = kwargs.pop("service_or_name", "awards")
224
+ # Here we only update and we do not insert, since CORDIS data is used to augment existing awards
225
+ # (with subjects and organizations information) and is not used to create new awards.
226
+ super().__init__(
227
+ service_or_name=service_or_name, insert=False, update=True, *args, **kwargs
228
+ )
229
+
230
+ def _entry_id(self, entry):
231
+ """Get the id from an entry."""
232
+ return entry["id"]
233
+
234
+
175
235
  VOCABULARIES_DATASTREAM_READERS = {
176
- "openaire-project-http": OpenAIREProjectHTTPReader,
236
+ "cordis-project-http": CORDISProjectHTTPReader,
177
237
  }
178
238
 
179
239
  VOCABULARIES_DATASTREAM_TRANSFORMERS = {
180
240
  "openaire-award": OpenAIREProjectTransformer,
241
+ "cordis-award": CORDISProjectTransformer,
181
242
  }
182
243
  """ORCiD Data Streams transformers."""
183
244
 
184
245
  VOCABULARIES_DATASTREAM_WRITERS = {
185
246
  "awards-service": AwardsServiceWriter,
247
+ "cordis-awards-service": CORDISAwardsServiceWriter,
186
248
  }
187
249
  """ORCiD Data Streams transformers."""
188
250
 
251
+ DATASTREAM_CONFIG_CORDIS = {
252
+ "readers": [
253
+ {"type": "cordis-project-http"},
254
+ {
255
+ "type": "zip",
256
+ "args": {
257
+ "regex": "\\.xml$",
258
+ "mode": "r",
259
+ },
260
+ },
261
+ {
262
+ "type": "xml",
263
+ "args": {
264
+ "root_element": "project",
265
+ },
266
+ },
267
+ ],
268
+ "transformers": [
269
+ {"type": "cordis-award"},
270
+ ],
271
+ "writers": [
272
+ {
273
+ "type": "cordis-awards-service",
274
+ "args": {
275
+ "identity": system_identity,
276
+ },
277
+ }
278
+ ],
279
+ }
280
+ """Data Stream configuration.
281
+
282
+ An origin is required for the reader.
283
+ """
284
+
189
285
  DATASTREAM_CONFIG = {
190
286
  "readers": [
191
287
  {
@@ -42,6 +42,61 @@
42
42
  },
43
43
  "program": {
44
44
  "type": "string"
45
+ },
46
+ "subjects": {
47
+ "description": "Award's subjects.",
48
+ "type": "array",
49
+ "properties": {
50
+ "id": {
51
+ "$ref": "local://definitions-v1.0.0.json#/identifier"
52
+ },
53
+ "scheme": {
54
+ "description": "Identifier of the subject scheme.",
55
+ "$ref": "local://definitions-v1.0.0.json#/identifier"
56
+ },
57
+ "subject": {
58
+ "description": "Human readable label.",
59
+ "type": "string"
60
+ },
61
+ "props": {
62
+ "type": "object",
63
+ "patternProperties": {
64
+ "^.*$": {
65
+ "type": "string"
66
+ }
67
+ }
68
+ },
69
+ "identifiers": {
70
+ "description": "Alternate identifiers for the subject.",
71
+ "type": "array",
72
+ "items": {
73
+ "$ref": "local://definitions-v2.0.0.json#/identifiers_with_scheme"
74
+ },
75
+ "uniqueItems": true
76
+ }
77
+ }
78
+ },
79
+ "organizations": {
80
+ "description": "Award's organizations.",
81
+ "type": "array",
82
+ "items": {
83
+ "type": "object",
84
+ "additionalProperties": false,
85
+ "properties": {
86
+ "scheme": {
87
+ "description": "Identifier of the organization scheme.",
88
+ "$ref": "local://definitions-v1.0.0.json#/identifier"
89
+ },
90
+ "id": {
91
+ "description": "Identifier of the organization for the given scheme.",
92
+ "$ref": "local://definitions-v1.0.0.json#/identifier"
93
+ },
94
+ "organization": {
95
+ "description": "Human readable label.",
96
+ "type": "string"
97
+ }
98
+ }
99
+ }
45
100
  }
46
101
  }
47
102
  }
@@ -58,12 +58,55 @@
58
58
  "acronym": {
59
59
  "type": "keyword",
60
60
  "fields": {
61
- "text": { "type": "text"}
61
+ "text": { "type": "text" }
62
62
  }
63
63
  },
64
64
  "program": {
65
65
  "type": "keyword"
66
66
  },
67
+ "subjects": {
68
+ "properties": {
69
+ "@v": {
70
+ "type": "keyword"
71
+ },
72
+ "id": {
73
+ "type": "keyword"
74
+ },
75
+ "props": {
76
+ "type": "object",
77
+ "dynamic": "true"
78
+ },
79
+ "subject": {
80
+ "type": "keyword"
81
+ },
82
+ "scheme": {
83
+ "type": "keyword"
84
+ },
85
+ "identifiers": {
86
+ "properties": {
87
+ "identifier": {
88
+ "type": "keyword"
89
+ },
90
+ "scheme": {
91
+ "type": "keyword"
92
+ }
93
+ }
94
+ }
95
+ }
96
+ },
97
+ "organizations": {
98
+ "properties": {
99
+ "scheme": {
100
+ "type": "keyword"
101
+ },
102
+ "id": {
103
+ "type": "keyword"
104
+ },
105
+ "organization": {
106
+ "type": "keyword"
107
+ }
108
+ }
109
+ },
67
110
  "funder": {
68
111
  "type": "object",
69
112
  "properties": {
@@ -58,12 +58,55 @@
58
58
  "acronym": {
59
59
  "type": "keyword",
60
60
  "fields": {
61
- "text": { "type": "text"}
61
+ "text": { "type": "text" }
62
62
  }
63
63
  },
64
64
  "program": {
65
65
  "type": "keyword"
66
66
  },
67
+ "subjects": {
68
+ "properties": {
69
+ "@v": {
70
+ "type": "keyword"
71
+ },
72
+ "id": {
73
+ "type": "keyword"
74
+ },
75
+ "props": {
76
+ "type": "object",
77
+ "dynamic": "true"
78
+ },
79
+ "subject": {
80
+ "type": "keyword"
81
+ },
82
+ "scheme": {
83
+ "type": "keyword"
84
+ },
85
+ "identifiers": {
86
+ "properties": {
87
+ "identifier": {
88
+ "type": "keyword"
89
+ },
90
+ "scheme": {
91
+ "type": "keyword"
92
+ }
93
+ }
94
+ }
95
+ }
96
+ },
97
+ "organizations": {
98
+ "properties": {
99
+ "scheme": {
100
+ "type": "keyword"
101
+ },
102
+ "id": {
103
+ "type": "keyword"
104
+ },
105
+ "organization": {
106
+ "type": "keyword"
107
+ }
108
+ }
109
+ },
67
110
  "funder": {
68
111
  "type": "object",
69
112
  "properties": {