oarepo-runtime 1.5.89__py3-none-any.whl → 1.5.91__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.
- oarepo_runtime/info/views.py +273 -39
- oarepo_runtime/services/components.py +173 -14
- {oarepo_runtime-1.5.89.dist-info → oarepo_runtime-1.5.91.dist-info}/METADATA +1 -1
- {oarepo_runtime-1.5.89.dist-info → oarepo_runtime-1.5.91.dist-info}/RECORD +8 -8
- {oarepo_runtime-1.5.89.dist-info → oarepo_runtime-1.5.91.dist-info}/LICENSE +0 -0
- {oarepo_runtime-1.5.89.dist-info → oarepo_runtime-1.5.91.dist-info}/WHEEL +0 -0
- {oarepo_runtime-1.5.89.dist-info → oarepo_runtime-1.5.91.dist-info}/entry_points.txt +0 -0
- {oarepo_runtime-1.5.89.dist-info → oarepo_runtime-1.5.91.dist-info}/top_level.txt +0 -0
oarepo_runtime/info/views.py
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
+
import importlib
|
1
2
|
import json
|
2
3
|
import logging
|
3
4
|
import os
|
4
5
|
import re
|
5
6
|
from functools import cached_property
|
6
|
-
from urllib.parse import urljoin
|
7
|
+
from urllib.parse import urljoin, urlparse, urlunparse
|
7
8
|
|
8
9
|
import importlib_metadata
|
9
10
|
import importlib_resources
|
@@ -23,7 +24,11 @@ from flask_resources import (
|
|
23
24
|
from flask_restful import abort
|
24
25
|
from invenio_base.utils import obj_or_import_string
|
25
26
|
from invenio_jsonschemas import current_jsonschemas
|
26
|
-
from invenio_records_resources.proxies import
|
27
|
+
from invenio_records_resources.proxies import (
|
28
|
+
current_service_registry,
|
29
|
+
current_transfer_registry,
|
30
|
+
)
|
31
|
+
from invenio_records_resources.records.api import Record
|
27
32
|
|
28
33
|
logger = logging.getLogger("oarepo_runtime.info")
|
29
34
|
|
@@ -68,32 +73,42 @@ class InfoResource(Resource):
|
|
68
73
|
"""Repository endpoint."""
|
69
74
|
links = {
|
70
75
|
"self": url_for(request.endpoint, _external=True),
|
76
|
+
"api": replace_path_in_url(
|
77
|
+
url_for(request.endpoint, _external=True), "/api"
|
78
|
+
),
|
71
79
|
"models": url_for("oarepo_runtime_info.models", _external=True),
|
72
80
|
}
|
73
81
|
try:
|
74
82
|
import invenio_requests # noqa
|
83
|
+
|
75
84
|
links["requests"] = api_url_for("requests.search", _external=True)
|
76
85
|
except ImportError:
|
77
86
|
pass
|
78
87
|
|
79
88
|
ret = {
|
89
|
+
"schema": "local://introspection-v1.0.0",
|
80
90
|
"name": current_app.config.get("THEME_SITENAME", ""),
|
81
91
|
"description": current_app.config.get("REPOSITORY_DESCRIPTION", ""),
|
82
92
|
"version": os.environ.get("DEPLOYMENT_VERSION", "local development"),
|
83
93
|
"invenio_version": get_package_version("oarepo"),
|
84
|
-
"transfers":
|
85
|
-
"local-file",
|
86
|
-
"url-fetch",
|
87
|
-
# TODO: where to get these? (permissions?)
|
88
|
-
# "direct-s3",
|
89
|
-
],
|
94
|
+
"transfers": list(current_transfer_registry.get_transfer_types()),
|
90
95
|
"links": links,
|
96
|
+
"features": [
|
97
|
+
*_add_feature_if_can_import("drafts", "invenio_drafts_resources"),
|
98
|
+
*_add_feature_if_can_import("workflows", "oarepo_workflows"),
|
99
|
+
*_add_feature_if_can_import("requests", "invenio_requests"),
|
100
|
+
*_add_feature_if_can_import("communities", "invenio_communities"),
|
101
|
+
*_add_feature_if_can_import("request_types", "oarepo_requests"),
|
102
|
+
],
|
91
103
|
}
|
104
|
+
if len(self.model_data) == 1:
|
105
|
+
ret["default_model"] = self.model_data[0]["name"]
|
106
|
+
|
92
107
|
self.call_components("repository", data=ret)
|
93
108
|
return ret, 200
|
94
109
|
|
95
|
-
@
|
96
|
-
def
|
110
|
+
@cached_property
|
111
|
+
def model_data(self):
|
97
112
|
data = []
|
98
113
|
# iterate entrypoint oarepo.models
|
99
114
|
for model in importlib_metadata.entry_points().select(group="oarepo.models"):
|
@@ -112,9 +127,9 @@ class InfoResource(Resource):
|
|
112
127
|
continue
|
113
128
|
|
114
129
|
# check if the service class is inside OAREPO_GLOBAL_SEARCH and if not, skip it
|
115
|
-
global_search_models = current_app.config.get(
|
130
|
+
global_search_models = current_app.config.get("GLOBAL_SEARCH_MODELS", [])
|
116
131
|
for global_model in global_search_models:
|
117
|
-
if global_model[
|
132
|
+
if global_model["model_service"] == model_data["service"]["class"]:
|
118
133
|
break
|
119
134
|
else:
|
120
135
|
continue
|
@@ -122,19 +137,22 @@ class InfoResource(Resource):
|
|
122
137
|
model_features = self._get_model_features(model_data)
|
123
138
|
|
124
139
|
links = {
|
125
|
-
"api": self._get_model_api_endpoint(model_data),
|
126
140
|
"html": self._get_model_html_endpoint(model_data),
|
127
|
-
"schemas": self._get_model_schema_endpoints(model_data),
|
128
141
|
"model": self._get_model_model_endpoint(model.name),
|
129
142
|
# "openapi": url_for(self._get_model_openapi_endpoint(model_data), _external=True)
|
130
143
|
}
|
131
144
|
|
132
|
-
links["
|
145
|
+
links["records"] = self._get_model_api_endpoint(model_data)
|
133
146
|
if "drafts" in model_features:
|
134
|
-
links["
|
147
|
+
links["drafts"] = self._get_model_draft_endpoint(model_data)
|
148
|
+
links["deposit"] = links["records"]
|
135
149
|
|
136
150
|
data.append(
|
137
151
|
{
|
152
|
+
"schema": "local://" + model_data["json-schema-settings"]["name"],
|
153
|
+
"type": model_data.get(
|
154
|
+
"model-name", model_data.get("module", {}).get("base", "")
|
155
|
+
).lower(),
|
138
156
|
"name": model_data.get(
|
139
157
|
"model-name", model_data.get("module", {}).get("base", "")
|
140
158
|
).lower(),
|
@@ -144,11 +162,163 @@ class InfoResource(Resource):
|
|
144
162
|
"links": links,
|
145
163
|
# TODO: we also need to get previous schema versions here if we support
|
146
164
|
# multiple version of the same schema at the same time
|
147
|
-
"
|
165
|
+
"content_types": self._get_model_content_types(
|
166
|
+
service, resource_config_class, model_data
|
167
|
+
),
|
168
|
+
"metadata": model_data.get("properties", {}).get("metadata", None)
|
169
|
+
is not None,
|
148
170
|
}
|
149
171
|
)
|
150
172
|
self.call_components("model", data=data)
|
151
|
-
|
173
|
+
data.sort(key=lambda x: x["type"])
|
174
|
+
return data
|
175
|
+
|
176
|
+
@cached_property
|
177
|
+
def vocabulary_data(self):
|
178
|
+
ret = []
|
179
|
+
try:
|
180
|
+
from invenio_vocabularies.contrib.affiliations.api import Affiliation
|
181
|
+
from invenio_vocabularies.contrib.awards.api import Award
|
182
|
+
from invenio_vocabularies.contrib.funders.api import Funder
|
183
|
+
from invenio_vocabularies.contrib.names.api import Name
|
184
|
+
from invenio_vocabularies.contrib.subjects.api import Subject
|
185
|
+
from invenio_vocabularies.records.api import Vocabulary
|
186
|
+
from invenio_vocabularies.records.models import VocabularyType
|
187
|
+
except ImportError:
|
188
|
+
return ret
|
189
|
+
|
190
|
+
def _generate_rdm_vocabulary(
|
191
|
+
base_url: str,
|
192
|
+
record: type[Record],
|
193
|
+
vocabulary_type: str,
|
194
|
+
vocabulary_name: str,
|
195
|
+
vocabulary_description: str,
|
196
|
+
special: bool,
|
197
|
+
can_export: bool = True,
|
198
|
+
can_deposit: bool = False,
|
199
|
+
) -> dict:
|
200
|
+
if not base_url.endswith("/"):
|
201
|
+
base_url += "/"
|
202
|
+
url_prefix = base_url + "api" if special else base_url + "api/vocabularies"
|
203
|
+
schema_path = base_url + record.schema.value.replace("local://", "schemas/")
|
204
|
+
links = dict(
|
205
|
+
records=f"{url_prefix}/{vocabulary_type}",
|
206
|
+
)
|
207
|
+
if can_deposit:
|
208
|
+
links["deposit"] = f"{url_prefix}/{vocabulary_type}"
|
209
|
+
|
210
|
+
return dict(
|
211
|
+
schema=record.schema.value,
|
212
|
+
type=vocabulary_type,
|
213
|
+
name=vocabulary_name,
|
214
|
+
description="Vocabulary for " + vocabulary_name,
|
215
|
+
version="unknown",
|
216
|
+
features=["rdm", "vocabulary"],
|
217
|
+
links=links,
|
218
|
+
content_types=[
|
219
|
+
dict(
|
220
|
+
content_type="application/json",
|
221
|
+
name="Invenio RDM JSON",
|
222
|
+
description="Vocabulary JSON",
|
223
|
+
schema=schema_path,
|
224
|
+
can_export=can_export,
|
225
|
+
can_deposit=can_deposit,
|
226
|
+
)
|
227
|
+
],
|
228
|
+
metadata=False,
|
229
|
+
)
|
230
|
+
|
231
|
+
base_url = api_url_for("vocabularies.search", type="languages", _external=True)
|
232
|
+
base_url = replace_path_in_url(base_url, "/")
|
233
|
+
ret = [
|
234
|
+
_generate_rdm_vocabulary(
|
235
|
+
base_url, Affiliation, "affiliations", "Affiliations", "", special=True
|
236
|
+
),
|
237
|
+
_generate_rdm_vocabulary(
|
238
|
+
base_url, Award, "awards", "Awards", "", special=True
|
239
|
+
),
|
240
|
+
_generate_rdm_vocabulary(
|
241
|
+
base_url, Funder, "funders", "Funders", "", special=True
|
242
|
+
),
|
243
|
+
_generate_rdm_vocabulary(
|
244
|
+
base_url, Subject, "subjects", "Subjects", "", special=True
|
245
|
+
),
|
246
|
+
_generate_rdm_vocabulary(
|
247
|
+
base_url, Name, "names", "Names", "", special=True
|
248
|
+
),
|
249
|
+
_generate_rdm_vocabulary(
|
250
|
+
base_url,
|
251
|
+
Affiliation,
|
252
|
+
"affiliations-vocab",
|
253
|
+
"Writable Affiliations",
|
254
|
+
"Write endpoint for affiliations",
|
255
|
+
special=False,
|
256
|
+
can_deposit=True,
|
257
|
+
),
|
258
|
+
_generate_rdm_vocabulary(
|
259
|
+
base_url,
|
260
|
+
Award,
|
261
|
+
"awards-vocab",
|
262
|
+
"Writable Awards",
|
263
|
+
"Write endpoint for awards",
|
264
|
+
special=False,
|
265
|
+
can_deposit=True,
|
266
|
+
),
|
267
|
+
_generate_rdm_vocabulary(
|
268
|
+
base_url,
|
269
|
+
Funder,
|
270
|
+
"funders-vocab",
|
271
|
+
"Writable Funders",
|
272
|
+
"Write endpoint for funders",
|
273
|
+
special=False,
|
274
|
+
can_deposit=True,
|
275
|
+
),
|
276
|
+
_generate_rdm_vocabulary(
|
277
|
+
base_url,
|
278
|
+
Subject,
|
279
|
+
"subjects-vocab",
|
280
|
+
"Writable Subjects",
|
281
|
+
"Write endpoint for subjects",
|
282
|
+
special=False,
|
283
|
+
can_deposit=True,
|
284
|
+
),
|
285
|
+
_generate_rdm_vocabulary(
|
286
|
+
base_url,
|
287
|
+
Name,
|
288
|
+
"names-vocab",
|
289
|
+
"Writable Names",
|
290
|
+
"Write endpoint for names",
|
291
|
+
special=False,
|
292
|
+
can_deposit=True,
|
293
|
+
),
|
294
|
+
]
|
295
|
+
|
296
|
+
vc_types = {vc.id for vc in VocabularyType.query.all()}
|
297
|
+
vocab_type_metadata = current_app.config.get(
|
298
|
+
"INVENIO_VOCABULARY_TYPE_METADATA", {}
|
299
|
+
)
|
300
|
+
vc_types.update(vocab_type_metadata.keys())
|
301
|
+
|
302
|
+
for vc in sorted(vc_types):
|
303
|
+
vc_metadata = vocab_type_metadata.get(vc, {})
|
304
|
+
ret.append(
|
305
|
+
_generate_rdm_vocabulary(
|
306
|
+
base_url,
|
307
|
+
Vocabulary,
|
308
|
+
vc,
|
309
|
+
to_current_language(vc_metadata.get("name")) or vc,
|
310
|
+
to_current_language(vc_metadata.get("description")) or "",
|
311
|
+
special=False,
|
312
|
+
can_export=True,
|
313
|
+
can_deposit=True,
|
314
|
+
)
|
315
|
+
)
|
316
|
+
|
317
|
+
return ret
|
318
|
+
|
319
|
+
@response_handler(many=True)
|
320
|
+
def models(self):
|
321
|
+
return self.model_data + self.vocabulary_data, 200
|
152
322
|
|
153
323
|
@schema_view_args
|
154
324
|
@response_handler()
|
@@ -161,7 +331,7 @@ class InfoResource(Resource):
|
|
161
331
|
def model(self):
|
162
332
|
model = resource_requestctx.view_args["model"]
|
163
333
|
for _model in importlib_metadata.entry_points().select(
|
164
|
-
|
334
|
+
group="oarepo.models", name=model
|
165
335
|
):
|
166
336
|
package_name, file_name = _model.value.split(":")
|
167
337
|
model_data = json.loads(
|
@@ -254,10 +424,17 @@ class InfoResource(Resource):
|
|
254
424
|
logger.exception("Failed to get model html endpoint")
|
255
425
|
return None
|
256
426
|
|
427
|
+
def _get_model_model_endpoint(self, model):
|
428
|
+
try:
|
429
|
+
return url_for("oarepo_runtime_info.model", model=model, _external=True)
|
430
|
+
except: # NOSONAR noqa
|
431
|
+
logger.exception("Failed to get model model endpoint")
|
432
|
+
return None
|
433
|
+
|
257
434
|
def _get_model_schema_endpoints(self, model):
|
258
435
|
try:
|
259
436
|
return {
|
260
|
-
|
437
|
+
"application/json": url_for(
|
261
438
|
"oarepo_runtime_info.schema",
|
262
439
|
schema=model["json-schema-settings"]["name"],
|
263
440
|
_external=True,
|
@@ -267,32 +444,60 @@ class InfoResource(Resource):
|
|
267
444
|
logger.exception("Failed to get model schema endpoint")
|
268
445
|
return None
|
269
446
|
|
270
|
-
def
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
447
|
+
def _get_model_content_types(self, service, resource_config, model):
|
448
|
+
"""Get the content types supported by the model. Returns a list of:
|
449
|
+
|
450
|
+
content_type="application/json",
|
451
|
+
name="Invenio RDM JSON",
|
452
|
+
description="Invenio RDM JSON as described in",
|
453
|
+
schema=url / "schemas" / "records" / "record-v6.0.0.json",
|
454
|
+
can_export=True,
|
455
|
+
can_deposit=True,
|
456
|
+
"""
|
457
|
+
|
458
|
+
content_types = []
|
459
|
+
# implicit content type
|
460
|
+
content_types.append(
|
461
|
+
{
|
462
|
+
"content_type": "application/json",
|
463
|
+
"name": f"Internal json serialization of {model['model-name']}",
|
464
|
+
"description": "This content type is serving this model's native format as described on model link.",
|
465
|
+
"schema": url_for(
|
466
|
+
"oarepo_runtime_info.schema",
|
467
|
+
schema=model["json-schema-settings"]["name"],
|
468
|
+
_external=True,
|
469
|
+
),
|
470
|
+
"can_export": True,
|
471
|
+
"can_deposit": True,
|
472
|
+
}
|
473
|
+
)
|
276
474
|
|
277
|
-
|
475
|
+
# export content types
|
278
476
|
try:
|
279
|
-
record_cls = service.config.record_cls
|
280
|
-
schema = getattr(record_cls, "schema", None)
|
281
|
-
accept_types = []
|
282
477
|
for accept_type, handler in resource_config.response_handlers.items():
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
478
|
+
if accept_type == "application/json":
|
479
|
+
continue
|
480
|
+
curr_item = {
|
481
|
+
"content_type": accept_type,
|
482
|
+
"name": getattr(handler, "name", accept_type),
|
483
|
+
"description": getattr(handler, "description", ""),
|
484
|
+
"can_export": True,
|
485
|
+
"can_deposit": False,
|
486
|
+
}
|
487
|
+
if handler.serializer is not None:
|
488
|
+
if hasattr(handler.serializer, "name"):
|
489
|
+
curr_item["name"] = handler.serializer.name
|
490
|
+
if hasattr(handler.serializer, "description"):
|
491
|
+
curr_item["description"] = handler.serializer.description
|
492
|
+
if hasattr(handler.serializer, "info"):
|
493
|
+
curr_item.update(handler.serializer.info(service))
|
494
|
+
content_types.append(curr_item)
|
289
495
|
except: # NOSONAR noqa
|
290
496
|
logger.exception("Failed to get model schemas")
|
291
|
-
return
|
292
|
-
|
497
|
+
return content_types
|
293
498
|
|
294
499
|
def _get_resource_config_class(self, model_data):
|
295
|
-
model_class = model_data[
|
500
|
+
model_class = model_data["resource-config"]["class"]
|
296
501
|
return obj_or_import_string(model_class)()
|
297
502
|
|
298
503
|
def _get_service(self, model_data):
|
@@ -350,3 +555,32 @@ def api_url_for(endpoint, _external=True, **values):
|
|
350
555
|
)
|
351
556
|
finally:
|
352
557
|
_cv_request.set(current_request_context)
|
558
|
+
|
559
|
+
|
560
|
+
def replace_path_in_url(url, path):
|
561
|
+
# Parse the URL into its components
|
562
|
+
parsed_url = urlparse(url)
|
563
|
+
|
564
|
+
# Replace the path with '/api'
|
565
|
+
new_parsed_url = parsed_url._replace(path=path)
|
566
|
+
|
567
|
+
# Reconstruct the URL with the new path
|
568
|
+
new_url = urlunparse(new_parsed_url)
|
569
|
+
|
570
|
+
return new_url
|
571
|
+
|
572
|
+
|
573
|
+
def _add_feature_if_can_import(feature, module):
|
574
|
+
try:
|
575
|
+
importlib.import_module(module)
|
576
|
+
return [feature]
|
577
|
+
except ImportError:
|
578
|
+
return []
|
579
|
+
|
580
|
+
|
581
|
+
def to_current_language(data):
|
582
|
+
if isinstance(data, dict):
|
583
|
+
from flask_babel import get_locale
|
584
|
+
|
585
|
+
return data.get(get_locale().language)
|
586
|
+
return data
|
@@ -1,17 +1,30 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import inspect
|
2
4
|
from collections import defaultdict
|
5
|
+
from dataclasses import dataclass, field
|
3
6
|
from typing import Type
|
4
7
|
|
5
8
|
from flask import current_app
|
6
9
|
from invenio_accounts.models import User
|
10
|
+
from invenio_base.utils import obj_or_import_string
|
11
|
+
from invenio_drafts_resources.services.records.config import (
|
12
|
+
RecordServiceConfig as DraftsRecordServiceConfig,
|
13
|
+
)
|
14
|
+
from invenio_rdm_records.services.config import RDMRecordServiceConfig
|
7
15
|
from invenio_records import Record
|
16
|
+
from invenio_records_resources.services import FileServiceConfig
|
17
|
+
from invenio_records_resources.services.records.config import (
|
18
|
+
RecordServiceConfig as RecordsRecordServiceConfig,
|
19
|
+
)
|
8
20
|
|
9
|
-
from oarepo_runtime.services.custom_fields import
|
21
|
+
from oarepo_runtime.services.custom_fields import (
|
22
|
+
CustomFields,
|
23
|
+
CustomFieldsMixin,
|
24
|
+
InlinedCustomFields,
|
25
|
+
)
|
10
26
|
from oarepo_runtime.services.generators import RecordOwners
|
11
|
-
|
12
|
-
from invenio_drafts_resources.services.records.config import RecordServiceConfig as DraftsRecordServiceConfig
|
13
|
-
from invenio_records_resources.services.records.config import RecordServiceConfig as RecordsRecordServiceConfig
|
14
|
-
from invenio_records_resources.services import FileServiceConfig
|
27
|
+
|
15
28
|
try:
|
16
29
|
from invenio_drafts_resources.services.records.uow import ParentRecordCommitOp
|
17
30
|
except ImportError:
|
@@ -62,18 +75,23 @@ class DateIssuedComponent(ServiceComponent):
|
|
62
75
|
if "dateIssued" not in record["metadata"]:
|
63
76
|
record["metadata"]["dateIssued"] = datetime.today().strftime("%Y-%m-%d")
|
64
77
|
|
78
|
+
|
65
79
|
class CFRegistry:
|
66
80
|
def __init__(self):
|
67
81
|
self.custom_field_names = defaultdict(list)
|
68
82
|
|
69
83
|
def lookup(self, record_type: Type[Record]):
|
70
84
|
if record_type not in self.custom_field_names:
|
71
|
-
for fld in inspect.getmembers(
|
85
|
+
for fld in inspect.getmembers(
|
86
|
+
record_type, lambda x: isinstance(x, CustomFieldsMixin)
|
87
|
+
):
|
72
88
|
self.custom_field_names[record_type].append(fld[1])
|
73
89
|
return self.custom_field_names[record_type]
|
74
90
|
|
91
|
+
|
75
92
|
cf_registry = CFRegistry()
|
76
93
|
|
94
|
+
|
77
95
|
class CustomFieldsComponent(ServiceComponent):
|
78
96
|
def create(self, identity, data=None, record=None, **kwargs):
|
79
97
|
"""Create a new record."""
|
@@ -90,16 +108,16 @@ class CustomFieldsComponent(ServiceComponent):
|
|
90
108
|
elif isinstance(cf, InlinedCustomFields):
|
91
109
|
config = current_app.config.get(cf.config_key, {})
|
92
110
|
for c in config:
|
93
|
-
record[c.name] =data.get(c.name)
|
111
|
+
record[c.name] = data.get(c.name)
|
94
112
|
|
95
|
-
def process_service_configs(service_config):
|
96
113
|
|
114
|
+
def process_service_configs(service_config, *additional_components):
|
97
115
|
processed_components = []
|
98
116
|
target_classes = {
|
99
117
|
RDMRecordServiceConfig,
|
100
118
|
DraftsRecordServiceConfig,
|
101
119
|
RecordsRecordServiceConfig,
|
102
|
-
FileServiceConfig
|
120
|
+
FileServiceConfig,
|
103
121
|
}
|
104
122
|
|
105
123
|
for end_index, cls in enumerate(type(service_config).mro()):
|
@@ -110,19 +128,160 @@ def process_service_configs(service_config):
|
|
110
128
|
# there are two service_config instances in the MRO (Method Resolution Order) output.
|
111
129
|
start_index = 2 if hasattr(service_config, "build") else 1
|
112
130
|
|
113
|
-
service_configs = type(service_config).mro()[start_index:end_index + 1]
|
131
|
+
service_configs = type(service_config).mro()[start_index : end_index + 1]
|
114
132
|
for config in service_configs:
|
115
|
-
|
116
133
|
if hasattr(config, "build"):
|
117
134
|
config = config.build(current_app)
|
118
135
|
|
119
|
-
if hasattr(config,
|
136
|
+
if hasattr(config, "components"):
|
120
137
|
component_property = config.components
|
121
138
|
if isinstance(component_property, list):
|
122
139
|
processed_components.extend(component_property)
|
123
140
|
elif isinstance(component_property, tuple):
|
124
|
-
processed_components.extend(list
|
141
|
+
processed_components.extend(list(component_property))
|
125
142
|
else:
|
126
143
|
raise ValueError(f"{config} component's definition is not supported")
|
127
144
|
|
128
|
-
|
145
|
+
processed_components.extend(additional_components)
|
146
|
+
processed_components = _sort_components(processed_components)
|
147
|
+
return processed_components
|
148
|
+
|
149
|
+
|
150
|
+
@dataclass
|
151
|
+
class ComponentPlacement:
|
152
|
+
"""Component placement in the list of components.
|
153
|
+
|
154
|
+
This is a helper class used in the component ordering algorithm.
|
155
|
+
"""
|
156
|
+
|
157
|
+
component: Type[ServiceComponent]
|
158
|
+
"""Component to be ordered."""
|
159
|
+
|
160
|
+
depends_on: list[ComponentPlacement] = field(default_factory=list)
|
161
|
+
"""List of components this one depends on.
|
162
|
+
|
163
|
+
The components must be classes of ServiceComponent or '*' to denote
|
164
|
+
that this component depends on all other components and should be placed last.
|
165
|
+
"""
|
166
|
+
|
167
|
+
affects: list[ComponentPlacement] = field(default_factory=list)
|
168
|
+
"""List of components that depend on this one.
|
169
|
+
|
170
|
+
This is a temporary list used for evaluation of '*' dependencies
|
171
|
+
but does not take part in the sorting algorithm."""
|
172
|
+
|
173
|
+
def __hash__(self) -> int:
|
174
|
+
return id(self.component)
|
175
|
+
|
176
|
+
def __eq__(self, other: ComponentPlacement) -> bool:
|
177
|
+
return self.component is other.component
|
178
|
+
|
179
|
+
|
180
|
+
def _sort_components(components):
|
181
|
+
"""Sort components based on their dependencies while trying to
|
182
|
+
keep the initial order as far as possible."""
|
183
|
+
|
184
|
+
placements: list[ComponentPlacement] = _prepare_component_placement(components)
|
185
|
+
placements = _propagate_dependencies(placements)
|
186
|
+
|
187
|
+
ret = []
|
188
|
+
while placements:
|
189
|
+
without_dependencies = [p for p in placements if not p.depends_on]
|
190
|
+
if not without_dependencies:
|
191
|
+
raise ValueError("Circular dependency detected in components.")
|
192
|
+
for p in without_dependencies:
|
193
|
+
ret.append(p.component)
|
194
|
+
placements.remove(p)
|
195
|
+
for p2 in placements:
|
196
|
+
if p in p2.depends_on:
|
197
|
+
p2.depends_on.remove(p)
|
198
|
+
return ret
|
199
|
+
|
200
|
+
|
201
|
+
def _matching_placements(placements, dep_class_or_factory):
|
202
|
+
for pl in placements:
|
203
|
+
pl_component = pl.component
|
204
|
+
if not inspect.isclass(pl_component):
|
205
|
+
pl_component = type(pl_component(service=object()))
|
206
|
+
if issubclass(pl_component, dep_class_or_factory):
|
207
|
+
yield pl
|
208
|
+
|
209
|
+
|
210
|
+
def _prepare_component_placement(components) -> list[ComponentPlacement]:
|
211
|
+
"""Convert components to ComponentPlacement instances and resolve dependencies."""
|
212
|
+
placements = []
|
213
|
+
for idx, c in enumerate(components):
|
214
|
+
placement = ComponentPlacement(component=c)
|
215
|
+
placements.append(placement)
|
216
|
+
|
217
|
+
# direct dependencies
|
218
|
+
for idx, placement in enumerate(placements):
|
219
|
+
placements_without_this = placements[:idx] + placements[idx + 1 :]
|
220
|
+
for dep in getattr(placement.component, "depends_on", []):
|
221
|
+
if dep == "*":
|
222
|
+
continue
|
223
|
+
dep = obj_or_import_string(dep)
|
224
|
+
for pl in _matching_placements(placements_without_this, dep):
|
225
|
+
if pl not in placement.depends_on:
|
226
|
+
placement.depends_on.append(pl)
|
227
|
+
if placement not in pl.affects:
|
228
|
+
pl.affects.append(placement)
|
229
|
+
|
230
|
+
for dep in getattr(placement.component, "affects", []):
|
231
|
+
if dep == "*":
|
232
|
+
continue
|
233
|
+
dep = obj_or_import_string(dep)
|
234
|
+
for pl in _matching_placements(placements_without_this, dep):
|
235
|
+
if pl not in placement.affects:
|
236
|
+
placement.affects.append(pl)
|
237
|
+
if placement not in pl.depends_on:
|
238
|
+
pl.depends_on.append(placement)
|
239
|
+
|
240
|
+
# star dependencies
|
241
|
+
for idx, placement in enumerate(placements):
|
242
|
+
placements_without_this = placements[:idx] + placements[idx + 1 :]
|
243
|
+
if "*" in getattr(placement.component, "depends_on", []):
|
244
|
+
for pl in placements_without_this:
|
245
|
+
# if this placement is not in placements that pl depends on
|
246
|
+
# (added via direct dependencies above), add it
|
247
|
+
if placement not in pl.depends_on:
|
248
|
+
if pl not in placement.depends_on:
|
249
|
+
placement.depends_on.append(pl)
|
250
|
+
if placement not in pl.affects:
|
251
|
+
pl.affects.append(placement)
|
252
|
+
|
253
|
+
if "*" in getattr(placement.component, "affects", []):
|
254
|
+
for pl in placements_without_this:
|
255
|
+
# if this placement is not in placements that pl affects
|
256
|
+
# (added via direct dependencies above), add it
|
257
|
+
if placement not in pl.affects:
|
258
|
+
if pl not in placement.affects:
|
259
|
+
placement.affects.append(pl)
|
260
|
+
if placement not in pl.depends_on:
|
261
|
+
pl.depends_on.append(placement)
|
262
|
+
return placements
|
263
|
+
|
264
|
+
|
265
|
+
def _propagate_dependencies(
|
266
|
+
placements: list[ComponentPlacement],
|
267
|
+
) -> list[ComponentPlacement]:
|
268
|
+
# now propagate dependencies
|
269
|
+
dependency_propagated = True
|
270
|
+
while dependency_propagated:
|
271
|
+
dependency_propagated = False
|
272
|
+
for placement in placements:
|
273
|
+
for dep in placement.depends_on:
|
274
|
+
for dep_of_dep in dep.depends_on:
|
275
|
+
if dep_of_dep not in placement.depends_on:
|
276
|
+
placement.depends_on.append(dep_of_dep)
|
277
|
+
dep_of_dep.affects.append(placement)
|
278
|
+
dependency_propagated = True
|
279
|
+
|
280
|
+
for dep in placement.affects:
|
281
|
+
for dep_of_dep in dep.affects:
|
282
|
+
if dep_of_dep not in placement.affects:
|
283
|
+
placement.affects.append(dep_of_dep)
|
284
|
+
dep_of_dep.depends_on.append(placement)
|
285
|
+
dependency_propagated = True
|
286
|
+
|
287
|
+
return placements
|
@@ -43,7 +43,7 @@ oarepo_runtime/datastreams/writers/yaml.py,sha256=XchUJHQ58E2Mfgs8elImXbL38jFtI8
|
|
43
43
|
oarepo_runtime/i18n/__init__.py,sha256=h0knW_HwiyIt5TBHfdGqN7_BBYfpz1Fw6zhVy0C28fM,111
|
44
44
|
oarepo_runtime/info/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
45
45
|
oarepo_runtime/info/check.py,sha256=6O5Wjsdorx4eqiBiPU3z33XhCiwPTO_FGkzMDK7UH6I,3049
|
46
|
-
oarepo_runtime/info/views.py,sha256=
|
46
|
+
oarepo_runtime/info/views.py,sha256=5ygLgiZilk33OhrnB0h83-MG6K8nsmJFOiJaTBQc43c,21070
|
47
47
|
oarepo_runtime/records/__init__.py,sha256=JUf9_o09_6q4vuG43JzhSeTu7c-m_CVDSmgTQ7epYEo,1776
|
48
48
|
oarepo_runtime/records/dumpers/__init__.py,sha256=OmzNhLdMNKibmCksnj9eTX9xPBG30dziiK3j3bAAp3k,233
|
49
49
|
oarepo_runtime/records/dumpers/edtf_interval.py,sha256=YCShZAoqBQYaxVilEVotS-jXZsxxoXO67yu2urhkaMA,1198
|
@@ -73,7 +73,7 @@ oarepo_runtime/resources/file_resource.py,sha256=Ta3bFce7l0xwqkkOMOEu9mxbB8BbKj5
|
|
73
73
|
oarepo_runtime/resources/json_serializer.py,sha256=82_-xQEtxKaPakv8R1oBAFbGnxskF_Ve4tcfcy4PetI,963
|
74
74
|
oarepo_runtime/resources/localized_ui_json_serializer.py,sha256=3V9cJaG_e1PMXKVX_wKfBp1LmbeForwHyBNYdyha4uQ,1878
|
75
75
|
oarepo_runtime/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
76
|
-
oarepo_runtime/services/components.py,sha256=
|
76
|
+
oarepo_runtime/services/components.py,sha256=h7oQNPueuPqBD6mCRCIr7ZuNvzeliXinHjOyVV3FBvw,10775
|
77
77
|
oarepo_runtime/services/generators.py,sha256=j87HitHA_w2awsz0C5IAAJ0qjg9JMtvdO3dvh6FQyfg,250
|
78
78
|
oarepo_runtime/services/results.py,sha256=Ap2mUJHl3V4BSduTrBWPuco0inQVq0QsuCbVhez48uY,5705
|
79
79
|
oarepo_runtime/services/search.py,sha256=9xGTN5Yg6eTdptQ9qjO_umbacf9ooMuHYGXWYfla4-M,6227
|
@@ -135,9 +135,9 @@ tests/marshmallow_to_json/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
|
|
135
135
|
tests/marshmallow_to_json/test_datacite_ui_schema.py,sha256=82iLj8nW45lZOUewpWbLX3mpSkpa9lxo-vK-Qtv_1bU,48552
|
136
136
|
tests/marshmallow_to_json/test_simple_schema.py,sha256=izZN9p0v6kovtSZ6AdxBYmK_c6ZOti2_z_wPT_zXIr0,1500
|
137
137
|
tests/pkg_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
138
|
-
oarepo_runtime-1.5.
|
139
|
-
oarepo_runtime-1.5.
|
140
|
-
oarepo_runtime-1.5.
|
141
|
-
oarepo_runtime-1.5.
|
142
|
-
oarepo_runtime-1.5.
|
143
|
-
oarepo_runtime-1.5.
|
138
|
+
oarepo_runtime-1.5.91.dist-info/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
|
139
|
+
oarepo_runtime-1.5.91.dist-info/METADATA,sha256=ummUvZcf6ncHllBBplL7Pp3L0ZzLuatkfQ52spuTV8s,4720
|
140
|
+
oarepo_runtime-1.5.91.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
141
|
+
oarepo_runtime-1.5.91.dist-info/entry_points.txt,sha256=k7O5LZUOGsVeSpB7ulU0txBUNp1CVQG7Q7TJIVTPbzU,491
|
142
|
+
oarepo_runtime-1.5.91.dist-info/top_level.txt,sha256=bHhlkT1_RQC4IkfTQCqA3iN4KCB6cSFQlsXpQMSP-bE,21
|
143
|
+
oarepo_runtime-1.5.91.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|