oarepo-runtime 2.0.0.dev9__py3-none-any.whl → 2.0.0.dev11__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.
@@ -19,6 +19,6 @@ from .api import Model
19
19
  from .ext import OARepoRuntime
20
20
  from .proxies import current_runtime
21
21
 
22
- __version__ = "2.0.0dev9"
22
+ __version__ = "2.0.0dev11"
23
23
 
24
24
  __all__ = ("Model", "OARepoRuntime", "__version__", "current_runtime")
oarepo_runtime/api.py CHANGED
@@ -27,7 +27,11 @@ if TYPE_CHECKING:
27
27
  from flask_resources.responses import ResponseHandler
28
28
  from flask_resources.serializers import BaseSerializer
29
29
  from invenio_drafts_resources.records.api import Draft
30
- from invenio_records_resources.records.api import RecordBase
30
+ from invenio_records_resources.records.api import Record
31
+ from invenio_records_resources.records.systemfields.pid import (
32
+ ModelPIDField,
33
+ ModelPIDFieldContext,
34
+ )
31
35
  from invenio_records_resources.resources.records.config import RecordResourceConfig
32
36
  from invenio_records_resources.resources.records.resource import RecordResource
33
37
  from invenio_records_resources.services import (
@@ -72,7 +76,7 @@ class Export:
72
76
  class Model[
73
77
  S: RecordService = RecordService,
74
78
  C: RecordServiceConfig = RecordServiceConfig,
75
- R: RecordBase = RecordBase,
79
+ R: Record = Record,
76
80
  D: Draft = Draft,
77
81
  # not sure why this is flagged by pyright as an error
78
82
  RR: RecordResource = RecordResource, # pyright: ignore[reportGeneralTypeIssues]
@@ -242,6 +246,34 @@ class Model[
242
246
  """Get the API blueprint name for the model."""
243
247
  return cast("str", self.resource_config.blueprint_name)
244
248
 
249
+ @property
250
+ def record_pid_type(self) -> str | None:
251
+ """Get the PID type for the model."""
252
+ return self._pid_type_from_record(self.record_cls)
253
+
254
+ @property
255
+ def draft_pid_type(self) -> str | None:
256
+ """Get the PID type for the model."""
257
+ return self._pid_type_from_record(self.draft_cls)
258
+
259
+ def _pid_type_from_record(self, record_cls: type[Record] | None) -> str | None:
260
+ """Get the PID type from a record class, returning None if not found."""
261
+ if record_cls is None:
262
+ return None
263
+ pid_context: ModelPIDFieldContext | None = getattr(record_cls, "pid", None)
264
+ if pid_context is None:
265
+ # registered record has no pid field
266
+ return None # pragma: no cover
267
+ pid_field: ModelPIDField | None = getattr(pid_context, "field", None)
268
+ if pid_field is None:
269
+ # there is no pid field in the context
270
+ return None # pragma: no cover
271
+ pid_provider = getattr(pid_field, "_provider", None)
272
+ if not pid_provider:
273
+ # there is no pid provider in the field
274
+ return None # pragma: no cover
275
+ return getattr(pid_provider, "pid_type", None)
276
+
245
277
  def api_url(self, view_name: str, **kwargs: Any) -> str:
246
278
  """Get the API URL for the model."""
247
279
  return cast("str", invenio_url_for(f"{self.api_blueprint_name}.{view_name}", **kwargs))
@@ -10,9 +10,18 @@
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ from importlib.metadata import entry_points
14
+
13
15
  import click
14
16
 
17
+ from .search import init as search_init # noqa just to register it
18
+
15
19
 
16
20
  @click.group
17
21
  def oarepo() -> None:
18
22
  """OARepo commands. See invenio oarepo --help for details."""
23
+
24
+
25
+ # register additional commands to the oarepo group
26
+ for ep in entry_points(group="oarepo.cli"):
27
+ oarepo.add_command(ep.load()) # pragma: nocover
oarepo_runtime/ext.py CHANGED
@@ -15,13 +15,19 @@ from functools import cached_property
15
15
  from typing import TYPE_CHECKING, Any, cast
16
16
 
17
17
  from flask import current_app
18
+ from invenio_pidstore.errors import PIDDoesNotExistError
19
+ from invenio_pidstore.models import PersistentIdentifier
18
20
  from invenio_records_resources.proxies import current_service_registry
19
- from invenio_records_resources.records.api import RecordBase
21
+ from invenio_records_resources.records.api import Record, RecordBase
20
22
 
21
23
  from . import config
22
24
 
23
25
  if TYPE_CHECKING: # pragma: no cover
26
+ from collections.abc import Iterable
27
+ from uuid import UUID
28
+
24
29
  from flask import Flask
30
+ from invenio_drafts_resources.records.api import Draft
25
31
  from invenio_records_resources.services.base.service import Service
26
32
  from invenio_records_resources.services.records import RecordService
27
33
 
@@ -54,13 +60,104 @@ class OARepoRuntime:
54
60
  """Return the models registered in the extension."""
55
61
  return cast("dict[str, Model]", current_app.config["OAREPO_MODELS"])
56
62
 
63
+ @property
64
+ def rdm_models(self) -> Iterable[Model]:
65
+ """Return the RDM models registered in the extension."""
66
+ return [v for v in self.models.values() if v.records_alias_enabled]
67
+
57
68
  @cached_property
58
- def models_by_record_class(self) -> dict[type[RecordBase], Model]:
69
+ def models_by_record_class(self) -> dict[type[Record], Model]:
59
70
  """Return a mapping of record classes to their models."""
60
71
  ret = {model.record_cls: model for model in self.models.values() if model.record_cls is not None}
61
72
  ret.update({model.draft_cls: model for model in self.models.values() if model.draft_cls is not None})
62
73
  return ret
63
74
 
75
+ @cached_property
76
+ def record_class_by_pid_type(self) -> dict[str, type[Record]]:
77
+ """Return a mapping of PID types to their record classes."""
78
+ ret: dict[str, type[Record]] = {}
79
+ for model in self.models.values():
80
+ pid_type = model.record_pid_type
81
+ if pid_type is not None:
82
+ ret[pid_type] = model.record_cls
83
+ return ret
84
+
85
+ @cached_property
86
+ def draft_class_by_pid_type(self) -> dict[str, type[Draft]]:
87
+ """Return a mapping of PID types to their draft classes."""
88
+ ret: dict[str, type[Draft]] = {}
89
+ for model in self.models.values():
90
+ pid_type = model.draft_pid_type
91
+ if pid_type is not None and model.draft_cls is not None:
92
+ ret[pid_type] = model.draft_cls
93
+ return ret
94
+
95
+ @cached_property
96
+ def model_by_pid_type(self) -> dict[str, Model]:
97
+ """Return a mapping of PID types to their models."""
98
+ ret: dict[str, Model] = {}
99
+ for model in self.models.values():
100
+ pid_type = model.record_pid_type
101
+ if pid_type is not None:
102
+ ret[pid_type] = model
103
+ pid_type = model.draft_pid_type
104
+ if pid_type is not None:
105
+ ret[pid_type] = model
106
+ return ret
107
+
108
+ @cached_property
109
+ def models_by_schema(self) -> dict[str, Model]:
110
+ """Return a mapping of schemas to their models."""
111
+ ret: dict[str, Model] = {}
112
+ for model in self.models.values():
113
+ if model.record_cls is not None:
114
+ ret[model.record_cls.schema.value] = model # type: ignore # noqa
115
+ return ret
116
+
117
+ @cached_property
118
+ def rdm_models_by_schema(self) -> dict[str, Model]:
119
+ """Return a mapping of RDM schemas to their models."""
120
+ return {schema: model for schema, model in self.models_by_schema.items() if model.records_alias_enabled}
121
+
122
+ def find_pid_type_from_pid(self, pid_value: str) -> str:
123
+ """Given a PID value, get its associated PID type.
124
+
125
+ This method requires that there are no duplicities in the PID values
126
+ across models.
127
+ """
128
+ return cast("str", self._filter_model_pid(pid_value=pid_value).pid_type)
129
+
130
+ def find_pid_from_uuid(self, uuid: UUID) -> PersistentIdentifier:
131
+ """Given an object UUID, get its associated PID."""
132
+ return self._filter_model_pid(object_uuid=uuid)
133
+
134
+ def _filter_model_pid(self, **filter_kwargs: Any) -> PersistentIdentifier:
135
+ """Filter PIDs based on the provided criteria and return only one that matches.
136
+
137
+ Select persistent identifiers from the DB and return the one that is associated
138
+ with any service registered within oarepo_runtime. If no such PID exists,
139
+ an error is raised.
140
+
141
+ If the filter matches multiple services, an error is raised.
142
+ """
143
+ pids = PersistentIdentifier.query.filter_by(**filter_kwargs).all()
144
+
145
+ filtered_pids = [pid for pid in pids if pid.pid_type in self.record_class_by_pid_type]
146
+ if not filtered_pids:
147
+ raise PIDDoesNotExistError(
148
+ None,
149
+ filter_kwargs,
150
+ "The pid value/record uuid is not associated with any record.",
151
+ )
152
+
153
+ if len(filtered_pids) > 1:
154
+ raise PIDDoesNotExistError(
155
+ None,
156
+ filter_kwargs,
157
+ f"Multiple records found for pid value/record uuid: {filtered_pids}",
158
+ )
159
+ return filtered_pids[0]
160
+
64
161
  @property
65
162
  def services(self) -> dict[str, Service]:
66
163
  """Return the services registered in the extension."""
@@ -82,3 +179,20 @@ class OARepoRuntime:
82
179
  model = self.models_by_record_class[t]
83
180
  return model.service
84
181
  raise KeyError(f"No service found for record class '{record_cls.__name__}'.")
182
+
183
+ @cached_property
184
+ def published_indices(self) -> set[str]:
185
+ """Return the set of published indices."""
186
+ indices = set()
187
+ for model in self.models.values():
188
+ indices.add(model.record_cls.index.search_alias) # type: ignore[attr-defined]
189
+ return indices
190
+
191
+ @cached_property
192
+ def draft_indices(self) -> set[str]:
193
+ """Return the set of draft indices."""
194
+ indices = set()
195
+ for model in self.models.values():
196
+ if model.draft_cls is not None:
197
+ indices.add(model.draft_cls.index.search_alias) # type: ignore[attr-defined]
198
+ return indices
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oarepo-runtime
3
- Version: 2.0.0.dev9
3
+ Version: 2.0.0.dev11
4
4
  Summary: A set of runtime extensions of Invenio repository
5
5
  Project-URL: Homepage, https://github.com/oarepo/oarepo-runtime
6
6
  License-Expression: MIT
@@ -1,10 +1,10 @@
1
- oarepo_runtime/__init__.py,sha256=N47TeGUEMfn9siNoTQE10_MmHVx1nPQJYi2wqCCy2Cc,685
2
- oarepo_runtime/api.py,sha256=P_y8b5AFES0ssv6x_GDg4MtD6TIfoNsOAVMMoiRM560,10330
1
+ oarepo_runtime/__init__.py,sha256=KM6gm4Ly4Vc12KFgfw9A0I01WRelO0XmpPk5ok_hgRg,686
2
+ oarepo_runtime/api.py,sha256=OjwTS7cFsN0aYKG91UWlKRVaJer2liuBtFvPsOjjQ0k,11648
3
3
  oarepo_runtime/config.py,sha256=RUEPFn_5bKp9Wb0OY-Fb3VK30m35vF5IsLjYaQHhP3g,3838
4
- oarepo_runtime/ext.py,sha256=AMb5pMnCSbqIpPyP99YUKlf9vopz_b2ZW-RnvfsEVlk,3254
4
+ oarepo_runtime/ext.py,sha256=JbbqMM2H0Sq-umN4506wTpAbo5og954u-8MdG0U-ysU,7833
5
5
  oarepo_runtime/proxies.py,sha256=PXaRiBh5qs5-h8M81cJOgtqypFQcYUSjiSn2TLSujRw,648
6
6
  oarepo_runtime/py.typed,sha256=RznSCjXReEUI9zkmD25E8XniG_MvPpLBF6MyNZA8MmE,42
7
- oarepo_runtime/cli/__init__.py,sha256=w9GzC9_T1b1elx0WFyzu1Qa28ZoINQK-RcQopR7eKXs,467
7
+ oarepo_runtime/cli/__init__.py,sha256=H7GOeOBf0udgKWOdlAQswIMvRrD8BwcEjOVxIqP0Suw,731
8
8
  oarepo_runtime/cli/search.py,sha256=yqYHZauXsDBPpN4odYsPOWNQ9xWmAofQ407EAyqx6CY,1137
9
9
  oarepo_runtime/records/__init__.py,sha256=AbWzmVCY7MhrpdEeI0e3lKzeugPMUSo8T08-NBVeig4,339
10
10
  oarepo_runtime/records/drafts.py,sha256=CS-dUkrylNwscgBGfDyhwGBRCzwsyT6AA3Mhu40ShbY,1607
@@ -29,8 +29,8 @@ oarepo_runtime/services/records/mapping.py,sha256=y3oeToKEnaRYpMV3q2-2cXNzyzyL3X
29
29
  oarepo_runtime/services/schema/__init__.py,sha256=jgAPI_uKC6Ug4KQWnwQVg3-aNaw-eHja323AUFo5ELo,351
30
30
  oarepo_runtime/services/schema/i18n.py,sha256=9D1zOQaPKAnYzejB0vO-m2BJYnam0N0Lrq4jID7twfE,3174
31
31
  oarepo_runtime/services/schema/i18n_ui.py,sha256=DbusphhGDeaobTt4nuwNgKZ6Houlu4Sv3SuMGkdjRRY,3582
32
- oarepo_runtime-2.0.0.dev9.dist-info/METADATA,sha256=No__66X5H9WRKziGYe6178vvJnR_dnAwhoCrApJdxhw,4494
33
- oarepo_runtime-2.0.0.dev9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
- oarepo_runtime-2.0.0.dev9.dist-info/entry_points.txt,sha256=by146BvBlFEJn0MWGJj6pzt-VKFKEALhjh1tloNMtow,198
35
- oarepo_runtime-2.0.0.dev9.dist-info/licenses/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
36
- oarepo_runtime-2.0.0.dev9.dist-info/RECORD,,
32
+ oarepo_runtime-2.0.0.dev11.dist-info/METADATA,sha256=r5YecSoAKljsWb-mD3M_BWF7DQTjejUm6MAaoklgwcI,4495
33
+ oarepo_runtime-2.0.0.dev11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
+ oarepo_runtime-2.0.0.dev11.dist-info/entry_points.txt,sha256=by146BvBlFEJn0MWGJj6pzt-VKFKEALhjh1tloNMtow,198
35
+ oarepo_runtime-2.0.0.dev11.dist-info/licenses/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
36
+ oarepo_runtime-2.0.0.dev11.dist-info/RECORD,,