oarepo-runtime 1.5.101__py3-none-any.whl → 1.5.103__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,4 +1,5 @@
1
1
  import json
2
+ from collections.abc import Mapping, Sequence, Set
2
3
 
3
4
  import click
4
5
  from flask import current_app
@@ -7,14 +8,28 @@ from werkzeug.local import LocalProxy
7
8
 
8
9
  from .base import oarepo
9
10
 
11
+ def remove_lazy_objects(obj):
12
+ if isinstance(obj, Sequence):
13
+ if isinstance(obj, list):
14
+ return [remove_lazy_objects(item) for item in obj if not isinstance(item, LocalProxy)]
15
+ elif isinstance(obj, tuple):
16
+ return tuple(remove_lazy_objects(item) for item in obj if not isinstance(item, LocalProxy))
17
+ elif not isinstance(obj, LocalProxy):
18
+ return obj # strings, bytes, bytesarray etc.
19
+ elif isinstance(obj, Set):
20
+ if isinstance(obj, frozenset):
21
+ return frozenset(remove_lazy_objects(item) for item in obj if not isinstance(item, LocalProxy))
22
+ return {remove_lazy_objects(item) for item in obj if not isinstance(item, LocalProxy)}
23
+ elif isinstance(obj, Mapping):
24
+ return {k: remove_lazy_objects(v) for k, v in obj.items() if not isinstance(v, LocalProxy)}
25
+ elif not isinstance(obj, LocalProxy):
26
+ return obj # everything else that is not localproxy
10
27
 
11
28
  @oarepo.command(name="configuration")
12
29
  @click.argument("output_file", default="-")
13
30
  @with_appcontext
14
31
  def configuration_command(output_file):
15
- configuration = {
16
- k: v for k, v in current_app.config.items() if not isinstance(v, LocalProxy)
17
- }
32
+ configuration = remove_lazy_objects(current_app.config)
18
33
 
19
34
  try:
20
35
  invenio_db = current_app.extensions["invenio-db"]
@@ -33,4 +48,4 @@ def configuration_command(output_file):
33
48
  )
34
49
  else:
35
50
  with open(output_file, "w") as f:
36
- json.dump(configuration, f, skipkeys=True, ensure_ascii=False, default=lambda x: str(x))
51
+ json.dump(configuration, f,skipkeys=True, ensure_ascii=False, default=lambda x: str(x))
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import inspect
4
4
  from collections import defaultdict
5
5
  from dataclasses import dataclass, field
6
- from typing import Type
6
+ from typing import Any, Type
7
7
 
8
8
  from flask import current_app
9
9
  from invenio_accounts.models import User
@@ -17,6 +17,7 @@ from invenio_records_resources.services import FileServiceConfig
17
17
  from invenio_records_resources.services.records.config import (
18
18
  RecordServiceConfig as RecordsRecordServiceConfig,
19
19
  )
20
+
20
21
  from oarepo_runtime.proxies import current_oarepo
21
22
  from oarepo_runtime.services.custom_fields import (
22
23
  CustomFields,
@@ -147,7 +148,7 @@ def process_service_configs(service_config, *additional_components):
147
148
  for excluded_component in current_oarepo.rdm_excluded_components:
148
149
  if excluded_component in processed_components:
149
150
  processed_components.remove(excluded_component)
150
-
151
+
151
152
  processed_components = _sort_components(processed_components)
152
153
  return processed_components
153
154
 
@@ -165,44 +166,228 @@ class ComponentPlacement:
165
166
  depends_on: list[ComponentPlacement] = field(default_factory=list)
166
167
  """List of components this one depends on.
167
168
 
168
- The components must be classes of ServiceComponent or '*' to denote
169
- that this component depends on all other components and should be placed last.
169
+ The components must be classes of ServiceComponent.
170
170
  """
171
171
 
172
172
  affects: list[ComponentPlacement] = field(default_factory=list)
173
- """List of components that depend on this one.
174
-
175
- This is a temporary list used for evaluation of '*' dependencies
176
- but does not take part in the sorting algorithm."""
173
+ """List of components that depend on this one."""
174
+
175
+ star_depends: bool = False
176
+ """True if this component depends on all other components."""
177
+
178
+ star_affects: bool = False
179
+ """True if this component affects all other components."""
177
180
 
178
181
  def __hash__(self) -> int:
179
182
  return id(self.component)
180
183
 
181
- def __eq__(self, other: ComponentPlacement) -> bool:
184
+ def __eq__(self, other: Any) -> bool:
182
185
  return self.component is other.component
183
186
 
184
-
185
- def _sort_components(components):
187
+ def __repr__(self) -> str:
188
+ depends_on = [d.component.__name__ for d in self.depends_on]
189
+ affects = [a.component.__name__ for a in self.affects]
190
+ if self.star_affects:
191
+ affects.append("*")
192
+ if self.star_depends:
193
+ depends_on.append("*")
194
+ r = [f"<{self.component.__name__}"]
195
+ if depends_on:
196
+ r.append(f" depends_on: {depends_on}")
197
+ if affects:
198
+ r.append(f" affects: {affects}")
199
+ r.append(">")
200
+ return "".join(r)
201
+
202
+ def __str__(self) -> str:
203
+ return repr(self)
204
+
205
+
206
+ def _sort_components(components) -> list[Type[ServiceComponent]]:
186
207
  """Sort components based on their dependencies while trying to
187
- keep the initial order as far as possible."""
208
+ keep the initial order as far as possible.
209
+
210
+ Sorting algorithm:
211
+
212
+ 1. Select all "affects" components that has "*" in their "affects" list.
213
+ 2. Merge these components preserving other other "affects" and "depends_on" settings
214
+ and output these
215
+
216
+ 3. put "depends_on: *" components aside for the moment
217
+ 4. Sort the remaining components by their dependencies
218
+
219
+ 5. process depends_on: * in a similar way as in 2.
220
+ """
188
221
 
189
222
  placements: list[ComponentPlacement] = _prepare_component_placement(components)
190
- placements = _propagate_dependencies(placements)
191
223
 
224
+ # placements that must be first as they affect all other components
225
+ affects_placements = [p for p in placements if p.star_affects]
226
+
227
+ # placements that must be last as they depend on all other components
228
+ depends_on_placements = [p for p in placements if p.star_depends]
229
+
230
+ # if a component affects another that affects all components,
231
+ # add it to affects_placements
232
+ #
233
+ # A[affects *] B[affects A] C => adds B to affects_placements
234
+ modified = True
235
+ while modified:
236
+ modified = False
237
+ for p in placements:
238
+ if (
239
+ any(q in affects_placements for q in p.affects)
240
+ and p not in affects_placements
241
+ ):
242
+ affects_placements.append(p)
243
+ modified = True
244
+
245
+ # if a component depends on another that depends on all components,
246
+ # add it to depends_on_placements
247
+ # A[depends_on *] B[depends_on A] C => adds B to depends_on_placements
248
+ modified = True
249
+ while modified:
250
+ modified = False
251
+ for p in placements:
252
+ if (
253
+ any(q in depends_on_placements for q in p.depends_on)
254
+ and p not in depends_on_placements
255
+ ):
256
+ depends_on_placements.append(p)
257
+ modified = True
258
+
259
+ # those that do not affect or depend on all components
260
+ middle_placements = [
261
+ p
262
+ for p in placements
263
+ if p not in depends_on_placements and p not in affects_placements
264
+ ]
265
+
266
+ # sort placements inside each group by their respective depends on and affects
267
+ # relationships, ignoring the star relationships
192
268
  ret = []
193
- while placements:
194
- without_dependencies = [p for p in placements if not p.depends_on]
195
- if not without_dependencies:
196
- raise ValueError("Circular dependency detected in components.")
197
- for p in without_dependencies:
198
- ret.append(p.component)
199
- placements.remove(p)
200
- for p2 in placements:
201
- if p in p2.depends_on:
202
- p2.depends_on.remove(p)
269
+ ret.extend(_sort_placements(affects_placements))
270
+ ret.extend(_sort_placements(middle_placements))
271
+ ret.extend(_sort_placements(depends_on_placements))
203
272
  return ret
204
273
 
205
274
 
275
+ def _sort_placements(placements):
276
+ """Sort placements based on their dependencies.
277
+
278
+ The algorithm tries to keep the initial order as far as possible,
279
+ while still respecting the dependencies.
280
+
281
+ At first, for each component that affects another component, the algorithm
282
+ moves the affecting component before the affected one one step at a time.
283
+
284
+ When no more such moves are possible, the algorithm moves to the next step
285
+ and does similar thing for components that depend on another component.
286
+
287
+ The algorithm is repeated until all components are sorted. If they can not
288
+ be after a set number of iterations, the algorithm raises an exception.
289
+ """
290
+
291
+ _filter_depends_on_and_affects(placements)
292
+
293
+ for _ in range(10):
294
+ for __ in range(10):
295
+ # move components that affect other components before them
296
+ modified = _move_affecting_components(placements)
297
+
298
+ # move components that depend on other components after them
299
+ if _move_depends_on_components(placements):
300
+ modified = True
301
+
302
+ # if the order was not modified, we are done
303
+ if not modified:
304
+ return (p.component for p in placements)
305
+ else:
306
+ # could not sort the components by simple move. This means that we are
307
+ # in a situation where A C B[affects A depends on C] - we will try to
308
+ # swap A with C and try again.
309
+ _swap_out_of_order_components(placements)
310
+
311
+ raise ValueError(f"Can not order components: {placements}")
312
+
313
+
314
+ def _swap_out_of_order_components(placements):
315
+ for idx, placement in enumerate(placements):
316
+
317
+ # we are looking for a situation: A C placement[affects A depends on C]
318
+ # so if there are not depends on and affects, try the next one
319
+ if not placement.depends_on or not placement.affects:
320
+ continue
321
+
322
+ depends_on = [(p, placements.index(p)) for p in placement.depends_on]
323
+ affects = [(p, placements.index(p)) for p in placement.affects]
324
+
325
+ # if there are no indices to swap, continue
326
+ if not depends_on or not affects:
327
+ continue
328
+
329
+ # keep the indices in order
330
+ depends_on.sort(key=lambda x: x[1])
331
+ affects.sort(key=lambda x: x[1])
332
+
333
+ depends_on_indices = [d[1] for d in depends_on]
334
+ affects_indices = [a[1] for a in affects]
335
+
336
+ swapped_depends_on = [d[0] for d in depends_on]
337
+ swapped_affects = [a[0] for a in affects]
338
+
339
+ # if the index of the component the current placement depends on
340
+ # is lower that any of the components that depend on the current placement,
341
+ # we are ok as the placement can be moved to the space between them,
342
+ # so just continue
343
+ if max(depends_on_indices) < min(affects_indices):
344
+ continue
345
+
346
+ # add all to an array and sort them. This way depends on will be after
347
+ # affects, keeping the positions inside the group
348
+ indices = sorted([*affects_indices, *depends_on_indices])
349
+
350
+ for idx, dep in zip(
351
+ indices,
352
+ [*swapped_depends_on, *swapped_affects],
353
+ ):
354
+ placements[idx] = dep
355
+
356
+
357
+ def _filter_depends_on_and_affects(placements):
358
+ """Filter out dependencies on components that are not in the placements list."""
359
+ for placement in placements:
360
+ placement.depends_on = [p for p in placement.depends_on if p in placements]
361
+ placement.affects = [p for p in placement.affects if p in placements]
362
+
363
+
364
+ def _move_affecting_components(placements):
365
+ modified = False
366
+
367
+ for idx, placement in list(enumerate(placements)):
368
+ if not placement.affects:
369
+ continue
370
+ min_index = min(placements.index(a) for a in placement.affects)
371
+ if min_index < idx:
372
+ placements.remove(placement)
373
+ placements.insert(min_index, placement)
374
+ modified = True
375
+ return modified
376
+
377
+
378
+ def _move_depends_on_components(placements):
379
+ modified = False
380
+ for idx, placement in reversed(list(enumerate(placements))):
381
+ if not placement.depends_on:
382
+ continue
383
+ max_index = max(placements.index(a) for a in placement.depends_on)
384
+ if max_index > idx:
385
+ placements.remove(placement)
386
+ placements.insert(max_index, placement)
387
+ modified = True
388
+ return modified
389
+
390
+
206
391
  def _matching_placements(placements, dep_class_or_factory):
207
392
  for pl in placements:
208
393
  pl_component = pl.component
@@ -222,6 +407,9 @@ def _prepare_component_placement(components) -> list[ComponentPlacement]:
222
407
  # direct dependencies
223
408
  for idx, placement in enumerate(placements):
224
409
  placements_without_this = placements[:idx] + placements[idx + 1 :]
410
+ placement.star_depends = "*" in getattr(placement.component, "depends_on", [])
411
+ placement.star_affects = "*" in getattr(placement.component, "affects", [])
412
+
225
413
  for dep in getattr(placement.component, "depends_on", []):
226
414
  if dep == "*":
227
415
  continue
@@ -229,8 +417,6 @@ def _prepare_component_placement(components) -> list[ComponentPlacement]:
229
417
  for pl in _matching_placements(placements_without_this, dep):
230
418
  if pl not in placement.depends_on:
231
419
  placement.depends_on.append(pl)
232
- if placement not in pl.affects:
233
- pl.affects.append(placement)
234
420
 
235
421
  for dep in getattr(placement.component, "affects", []):
236
422
  if dep == "*":
@@ -239,54 +425,5 @@ def _prepare_component_placement(components) -> list[ComponentPlacement]:
239
425
  for pl in _matching_placements(placements_without_this, dep):
240
426
  if pl not in placement.affects:
241
427
  placement.affects.append(pl)
242
- if placement not in pl.depends_on:
243
- pl.depends_on.append(placement)
244
-
245
- # star dependencies
246
- for idx, placement in enumerate(placements):
247
- placements_without_this = placements[:idx] + placements[idx + 1 :]
248
- if "*" in getattr(placement.component, "depends_on", []):
249
- for pl in placements_without_this:
250
- # if this placement is not in placements that pl depends on
251
- # (added via direct dependencies above), add it
252
- if placement not in pl.depends_on:
253
- if pl not in placement.depends_on:
254
- placement.depends_on.append(pl)
255
- if placement not in pl.affects:
256
- pl.affects.append(placement)
257
-
258
- if "*" in getattr(placement.component, "affects", []):
259
- for pl in placements_without_this:
260
- # if this placement is not in placements that pl affects
261
- # (added via direct dependencies above), add it
262
- if placement not in pl.affects:
263
- if pl not in placement.affects:
264
- placement.affects.append(pl)
265
- if placement not in pl.depends_on:
266
- pl.depends_on.append(placement)
267
- return placements
268
-
269
-
270
- def _propagate_dependencies(
271
- placements: list[ComponentPlacement],
272
- ) -> list[ComponentPlacement]:
273
- # now propagate dependencies
274
- dependency_propagated = True
275
- while dependency_propagated:
276
- dependency_propagated = False
277
- for placement in placements:
278
- for dep in placement.depends_on:
279
- for dep_of_dep in dep.depends_on:
280
- if dep_of_dep not in placement.depends_on:
281
- placement.depends_on.append(dep_of_dep)
282
- dep_of_dep.affects.append(placement)
283
- dependency_propagated = True
284
-
285
- for dep in placement.affects:
286
- for dep_of_dep in dep.affects:
287
- if dep_of_dep not in placement.affects:
288
- placement.affects.append(dep_of_dep)
289
- dep_of_dep.depends_on.append(placement)
290
- dependency_propagated = True
291
428
 
292
429
  return placements
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: oarepo-runtime
3
- Version: 1.5.101
3
+ Version: 1.5.103
4
4
  Summary: A set of runtime extensions of Invenio repository
5
5
  Description-Content-Type: text/markdown
6
6
  License-File: LICENSE
@@ -10,7 +10,7 @@ oarepo_runtime/cli/assets.py,sha256=ZG80xIOKch7rsU_7QlvZxBQAXNQ9ow5xLsUREsl5MEE,
10
10
  oarepo_runtime/cli/base.py,sha256=94RBTa8TOSPxEyEUmYLGXaWen-XktP2-MIbTtZSlCZo,544
11
11
  oarepo_runtime/cli/cf.py,sha256=W0JEJK2JqKubQw8qtZJxohmADDRUBode4JZAqYLDGvc,339
12
12
  oarepo_runtime/cli/check.py,sha256=sCe2PeokSHvNOXHFZ8YHF8NMhsu5nYjyuZuvXHJ6X18,5092
13
- oarepo_runtime/cli/configuration.py,sha256=_iMmESs2dd1Oif95gxgpnkSxc13ymwr82_sTJfxlhrM,1091
13
+ oarepo_runtime/cli/configuration.py,sha256=M_19zrS0vo-7uHi047a3i4rQsK6pmp4QS0IEBAxMLZ0,2039
14
14
  oarepo_runtime/cli/fixtures.py,sha256=l6zHpz1adjotrbFy_wcN2TOL8x20i-1jbQmaoEEo-UU,5419
15
15
  oarepo_runtime/cli/index.py,sha256=KH5PArp0fCNbgJI1zSz0pb69U9eyCdnJuy0aMIgf2tg,8685
16
16
  oarepo_runtime/cli/validate.py,sha256=HpSvHQCGHlrdgdpKix9cIlzlBoJEiT1vACZdMnOUGEY,2827
@@ -80,7 +80,7 @@ oarepo_runtime/resources/file_resource.py,sha256=Ta3bFce7l0xwqkkOMOEu9mxbB8BbKj5
80
80
  oarepo_runtime/resources/json_serializer.py,sha256=82_-xQEtxKaPakv8R1oBAFbGnxskF_Ve4tcfcy4PetI,963
81
81
  oarepo_runtime/resources/localized_ui_json_serializer.py,sha256=3V9cJaG_e1PMXKVX_wKfBp1LmbeForwHyBNYdyha4uQ,1878
82
82
  oarepo_runtime/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
- oarepo_runtime/services/components.py,sha256=QKrdFmroE1hSrRjRkqinynCTb0KSqBhytTb9FoY0OEE,11023
83
+ oarepo_runtime/services/components.py,sha256=0aqmuNbGCQhfOI22f6mM2DxuKSTqstGOGyisOiqw9KE,15481
84
84
  oarepo_runtime/services/generators.py,sha256=j87HitHA_w2awsz0C5IAAJ0qjg9JMtvdO3dvh6FQyfg,250
85
85
  oarepo_runtime/services/results.py,sha256=Ap2mUJHl3V4BSduTrBWPuco0inQVq0QsuCbVhez48uY,5705
86
86
  oarepo_runtime/services/search.py,sha256=t0WEe2VrbCzZ06-Jgz7C9-pc9y27BqAgTEXldEHskfk,9409
@@ -142,9 +142,9 @@ tests/marshmallow_to_json/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
142
142
  tests/marshmallow_to_json/test_datacite_ui_schema.py,sha256=82iLj8nW45lZOUewpWbLX3mpSkpa9lxo-vK-Qtv_1bU,48552
143
143
  tests/marshmallow_to_json/test_simple_schema.py,sha256=izZN9p0v6kovtSZ6AdxBYmK_c6ZOti2_z_wPT_zXIr0,1500
144
144
  tests/pkg_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
145
- oarepo_runtime-1.5.101.dist-info/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
146
- oarepo_runtime-1.5.101.dist-info/METADATA,sha256=UDnOvxV1DK-bBGw8zrnSsPaxELMtHe2hacAxaD6HDQc,4721
147
- oarepo_runtime-1.5.101.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
148
- oarepo_runtime-1.5.101.dist-info/entry_points.txt,sha256=k7O5LZUOGsVeSpB7ulU0txBUNp1CVQG7Q7TJIVTPbzU,491
149
- oarepo_runtime-1.5.101.dist-info/top_level.txt,sha256=bHhlkT1_RQC4IkfTQCqA3iN4KCB6cSFQlsXpQMSP-bE,21
150
- oarepo_runtime-1.5.101.dist-info/RECORD,,
145
+ oarepo_runtime-1.5.103.dist-info/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
146
+ oarepo_runtime-1.5.103.dist-info/METADATA,sha256=bLIBuMCjXm85bclvhOvWFWBfpCglssd-Xt4AiPUB6rI,4721
147
+ oarepo_runtime-1.5.103.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
148
+ oarepo_runtime-1.5.103.dist-info/entry_points.txt,sha256=k7O5LZUOGsVeSpB7ulU0txBUNp1CVQG7Q7TJIVTPbzU,491
149
+ oarepo_runtime-1.5.103.dist-info/top_level.txt,sha256=bHhlkT1_RQC4IkfTQCqA3iN4KCB6cSFQlsXpQMSP-bE,21
150
+ oarepo_runtime-1.5.103.dist-info/RECORD,,