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.
@@ -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 current_service_registry
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
- @response_handler(many=True)
96
- def models(self):
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('GLOBAL_SEARCH_MODELS', [])
130
+ global_search_models = current_app.config.get("GLOBAL_SEARCH_MODELS", [])
116
131
  for global_model in global_search_models:
117
- if global_model['model_service'] == model_data["service"]["class"]:
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["published"] = links["api"]
145
+ links["records"] = self._get_model_api_endpoint(model_data)
133
146
  if "drafts" in model_features:
134
- links["user_records"] = self._get_model_draft_endpoint(model_data)
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
- "accept": self._get_model_accept_types(service, resource_config_class),
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
- return data, 200
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
- group="oarepo.models", name=model
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
- 'application/json': url_for(
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 _get_model_model_endpoint(self, model):
271
- try:
272
- return url_for("oarepo_runtime_info.model", model=model, _external=True)
273
- except: # NOSONAR noqa
274
- logger.exception("Failed to get model model endpoint")
275
- return None
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
- def _get_model_accept_types(self, service, resource_config):
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
- curr_item = {'accept': accept_type}
284
- if handler.serializer is not None and hasattr(handler.serializer, "info"):
285
- curr_item.update(handler.serializer.info(service))
286
- accept_types.append(curr_item)
287
-
288
- return accept_types
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['resource-config']['class']
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 CustomFieldsMixin, CustomFields, InlinedCustomFields
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
- from invenio_rdm_records.services.config import RDMRecordServiceConfig
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(record_type, lambda x: isinstance(x, CustomFieldsMixin)):
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, 'components'):
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 (component_property))
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
- return processed_components
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: oarepo-runtime
3
- Version: 1.5.89
3
+ Version: 1.5.91
4
4
  Summary: A set of runtime extensions of Invenio repository
5
5
  Description-Content-Type: text/markdown
6
6
  License-File: LICENSE
@@ -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=q9PG46aM-1ihaVxJGjfcD5HvJWanM28gMefao8FIQk4,12440
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=rCdK3amTM2PAihiRFnaoN_Xkm35W-PreAPjm-gAJFww,4902
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.89.dist-info/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
139
- oarepo_runtime-1.5.89.dist-info/METADATA,sha256=2WAbKmJSznmufN3BQhjw5i4Sn_ZA_RRgkdwWuAOb0DY,4720
140
- oarepo_runtime-1.5.89.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
141
- oarepo_runtime-1.5.89.dist-info/entry_points.txt,sha256=k7O5LZUOGsVeSpB7ulU0txBUNp1CVQG7Q7TJIVTPbzU,491
142
- oarepo_runtime-1.5.89.dist-info/top_level.txt,sha256=bHhlkT1_RQC4IkfTQCqA3iN4KCB6cSFQlsXpQMSP-bE,21
143
- oarepo_runtime-1.5.89.dist-info/RECORD,,
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,,