oarepo-runtime 2.0.0.dev10__tar.gz → 2.0.0.dev12__tar.gz

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.
Files changed (37) hide show
  1. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/PKG-INFO +1 -1
  2. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/__init__.py +1 -1
  3. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/api.py +39 -2
  4. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/cli/__init__.py +1 -1
  5. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/cli/search.py +4 -3
  6. oarepo_runtime-2.0.0.dev12/oarepo_runtime/ext.py +198 -0
  7. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/records/pid_providers.py +2 -1
  8. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/pyproject.toml +3 -0
  9. oarepo_runtime-2.0.0.dev10/oarepo_runtime/ext.py +0 -84
  10. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/.gitignore +0 -0
  11. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/LICENSE +0 -0
  12. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/README.md +0 -0
  13. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/config.py +0 -0
  14. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/proxies.py +0 -0
  15. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/py.typed +0 -0
  16. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/records/__init__.py +0 -0
  17. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/records/drafts.py +0 -0
  18. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/records/mapping.py +0 -0
  19. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/records/systemfields/__init__.py +0 -0
  20. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/records/systemfields/mapping.py +0 -0
  21. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/records/systemfields/publication_status.py +0 -0
  22. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/resources/__init__.py +0 -0
  23. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/resources/config.py +0 -0
  24. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/__init__.py +0 -0
  25. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/config/__init__.py +0 -0
  26. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/config/components.py +0 -0
  27. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/config/link_conditions.py +0 -0
  28. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/config/permissions.py +0 -0
  29. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/facets/__init__.py +0 -0
  30. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/facets/params.py +0 -0
  31. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/records/__init__.py +0 -0
  32. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/records/links.py +0 -0
  33. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/records/mapping.py +0 -0
  34. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/results.py +0 -0
  35. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/schema/__init__.py +0 -0
  36. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/schema/i18n.py +0 -0
  37. {oarepo_runtime-2.0.0.dev10 → oarepo_runtime-2.0.0.dev12}/oarepo_runtime/services/schema/i18n_ui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oarepo-runtime
3
- Version: 2.0.0.dev10
3
+ Version: 2.0.0.dev12
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
@@ -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.0dev10"
22
+ __version__ = "2.0.0dev12"
23
23
 
24
24
  __all__ = ("Model", "OARepoRuntime", "__version__", "current_runtime")
@@ -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,39 @@ 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 record_json_schema(self) -> str:
256
+ """Get the json schema of the record."""
257
+ return self.record_cls.schema.value # type: ignore[attr-defined, no-any-return]
258
+
259
+ @property
260
+ def draft_pid_type(self) -> str | None:
261
+ """Get the PID type for the model."""
262
+ return self._pid_type_from_record(self.draft_cls)
263
+
264
+ def _pid_type_from_record(self, record_cls: type[Record] | None) -> str | None:
265
+ """Get the PID type from a record class, returning None if not found."""
266
+ if record_cls is None:
267
+ return None
268
+ pid_context: ModelPIDFieldContext | None = getattr(record_cls, "pid", None)
269
+ if pid_context is None:
270
+ # registered record has no pid field
271
+ return None # pragma: no cover
272
+ pid_field: ModelPIDField | None = getattr(pid_context, "field", None)
273
+ if pid_field is None:
274
+ # there is no pid field in the context
275
+ return None # pragma: no cover
276
+ pid_provider = getattr(pid_field, "_provider", None)
277
+ if not pid_provider:
278
+ # there is no pid provider in the field
279
+ return None # pragma: no cover
280
+ return getattr(pid_provider, "pid_type", None)
281
+
245
282
  def api_url(self, view_name: str, **kwargs: Any) -> str:
246
283
  """Get the API URL for the model."""
247
284
  return cast("str", invenio_url_for(f"{self.api_blueprint_name}.{view_name}", **kwargs))
@@ -24,4 +24,4 @@ def oarepo() -> None:
24
24
 
25
25
  # register additional commands to the oarepo group
26
26
  for ep in entry_points(group="oarepo.cli"):
27
- oarepo.add_command(ep.load())
27
+ oarepo.add_command(ep.load()) # pragma: nocover
@@ -10,13 +10,13 @@
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ from importlib import metadata as importlib_metadata
14
+
13
15
  import click
14
16
  from flask.cli import with_appcontext
15
17
  from invenio_search.cli import index, search_version_check
16
18
  from invenio_search.cli import init as original_init
17
19
 
18
- from oarepo_runtime.services.records.mapping import update_all_records_mappings
19
-
20
20
 
21
21
  @index.command()
22
22
  @click.option("--force", is_flag=True, default=False)
@@ -31,4 +31,5 @@ def init(ctx: click.Context, force: bool) -> None:
31
31
  defined inside the models.
32
32
  """
33
33
  ctx.invoke(original_init, force=force)
34
- update_all_records_mappings()
34
+ for ep in importlib_metadata.entry_points(group="oarepo.cli.search.init"):
35
+ ep.load()()
@@ -0,0 +1,198 @@
1
+ #
2
+ # Copyright (c) 2025 CESNET z.s.p.o.
3
+ #
4
+ # This file is a part of oarepo-runtime (see http://github.com/oarepo/oarepo-runtime).
5
+ #
6
+ # oarepo-runtime is free software; you can redistribute it and/or modify it
7
+ # under the terms of the MIT License; see LICENSE file for more details.
8
+ #
9
+
10
+ """Extension preset for runtime module."""
11
+
12
+ from __future__ import annotations
13
+
14
+ from functools import cached_property
15
+ from typing import TYPE_CHECKING, Any, cast
16
+
17
+ from flask import current_app
18
+ from invenio_pidstore.errors import PIDDoesNotExistError
19
+ from invenio_pidstore.models import PersistentIdentifier
20
+ from invenio_records_resources.proxies import current_service_registry
21
+ from invenio_records_resources.records.api import Record, RecordBase
22
+
23
+ from . import config
24
+
25
+ if TYPE_CHECKING: # pragma: no cover
26
+ from collections.abc import Iterable
27
+ from uuid import UUID
28
+
29
+ from flask import Flask
30
+ from invenio_drafts_resources.records.api import Draft
31
+ from invenio_records_resources.services.base.service import Service
32
+ from invenio_records_resources.services.records import RecordService
33
+
34
+ from .api import Model
35
+
36
+
37
+ class OARepoRuntime:
38
+ """OARepo base of invenio oarepo client."""
39
+
40
+ def __init__(self, app: Flask | None = None):
41
+ """Extension initialization."""
42
+ if app:
43
+ self.init_app(app)
44
+
45
+ def init_app(self, app: Flask) -> None:
46
+ """Flask application initialization."""
47
+ self.app = app
48
+ self.init_config(app)
49
+ app.extensions["oarepo-runtime"] = self
50
+
51
+ def init_config(self, app: Flask) -> None:
52
+ """Initialize the configuration for the extension."""
53
+ app.config.setdefault("OAREPO_MODELS", {})
54
+ for k, v in config.OAREPO_MODELS.items():
55
+ if k not in app.config["OAREPO_MODELS"]:
56
+ app.config["OAREPO_MODELS"][k] = v
57
+
58
+ @property
59
+ def models(self) -> dict[str, Model]:
60
+ """Return the models registered in the extension."""
61
+ return cast("dict[str, Model]", current_app.config["OAREPO_MODELS"])
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
+
68
+ @cached_property
69
+ def models_by_record_class(self) -> dict[type[Record], Model]:
70
+ """Return a mapping of record classes to their models."""
71
+ ret = {model.record_cls: model for model in self.models.values() if model.record_cls is not None}
72
+ ret.update({model.draft_cls: model for model in self.models.values() if model.draft_cls is not None})
73
+ return ret
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
+
161
+ @property
162
+ def services(self) -> dict[str, Service]:
163
+ """Return the services registered in the extension."""
164
+ _services = current_service_registry._services # type: ignore[attr-defined] # noqa: SLF001
165
+ return cast("dict[str, Service]", _services)
166
+
167
+ def get_record_service_for_record(self, record: Any) -> RecordService:
168
+ """Retrieve the associated service for a given record."""
169
+ if record is None:
170
+ raise ValueError("Need to pass a record instance, got None")
171
+ return self.get_record_service_for_record_class(type(record))
172
+
173
+ def get_record_service_for_record_class(self, record_cls: type[RecordBase]) -> RecordService:
174
+ """Retrieve the service associated with a given record class."""
175
+ for t in record_cls.mro():
176
+ if t is RecordBase:
177
+ break
178
+ if t in self.models_by_record_class:
179
+ model = self.models_by_record_class[t]
180
+ return model.service
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
@@ -23,7 +23,8 @@ else:
23
23
  class UniversalPIDMixin(RecordIdProviderV2):
24
24
  """Mixin class to handle creation and management of universal PIDs for records."""
25
25
 
26
- unpid_pid_type = "unpid"
26
+ unpid_pid_type = "recid"
27
+ """Setting this to recid so that RDM can use it."""
27
28
  unpid_default_status = PIDStatus.REGISTERED
28
29
 
29
30
  @classmethod
@@ -34,6 +34,9 @@ oarepo_runtime = "oarepo_runtime.ext:OARepoRuntime"
34
34
  [project.entry-points."flask.commands"]
35
35
  oarepo = "oarepo_runtime.cli:oarepo"
36
36
 
37
+ [project.entry-points."oarepo.cli.search.init"]
38
+ runtime_update_mappings = "oarepo_runtime.services.records.mapping:update_all_records_mappings"
39
+
37
40
  [tool.hatch.version]
38
41
  path = "oarepo_runtime/__init__.py"
39
42
 
@@ -1,84 +0,0 @@
1
- #
2
- # Copyright (c) 2025 CESNET z.s.p.o.
3
- #
4
- # This file is a part of oarepo-runtime (see http://github.com/oarepo/oarepo-runtime).
5
- #
6
- # oarepo-runtime is free software; you can redistribute it and/or modify it
7
- # under the terms of the MIT License; see LICENSE file for more details.
8
- #
9
-
10
- """Extension preset for runtime module."""
11
-
12
- from __future__ import annotations
13
-
14
- from functools import cached_property
15
- from typing import TYPE_CHECKING, Any, cast
16
-
17
- from flask import current_app
18
- from invenio_records_resources.proxies import current_service_registry
19
- from invenio_records_resources.records.api import RecordBase
20
-
21
- from . import config
22
-
23
- if TYPE_CHECKING: # pragma: no cover
24
- from flask import Flask
25
- from invenio_records_resources.services.base.service import Service
26
- from invenio_records_resources.services.records import RecordService
27
-
28
- from .api import Model
29
-
30
-
31
- class OARepoRuntime:
32
- """OARepo base of invenio oarepo client."""
33
-
34
- def __init__(self, app: Flask | None = None):
35
- """Extension initialization."""
36
- if app:
37
- self.init_app(app)
38
-
39
- def init_app(self, app: Flask) -> None:
40
- """Flask application initialization."""
41
- self.app = app
42
- self.init_config(app)
43
- app.extensions["oarepo-runtime"] = self
44
-
45
- def init_config(self, app: Flask) -> None:
46
- """Initialize the configuration for the extension."""
47
- app.config.setdefault("OAREPO_MODELS", {})
48
- for k, v in config.OAREPO_MODELS.items():
49
- if k not in app.config["OAREPO_MODELS"]:
50
- app.config["OAREPO_MODELS"][k] = v
51
-
52
- @property
53
- def models(self) -> dict[str, Model]:
54
- """Return the models registered in the extension."""
55
- return cast("dict[str, Model]", current_app.config["OAREPO_MODELS"])
56
-
57
- @cached_property
58
- def models_by_record_class(self) -> dict[type[RecordBase], Model]:
59
- """Return a mapping of record classes to their models."""
60
- ret = {model.record_cls: model for model in self.models.values() if model.record_cls is not None}
61
- ret.update({model.draft_cls: model for model in self.models.values() if model.draft_cls is not None})
62
- return ret
63
-
64
- @property
65
- def services(self) -> dict[str, Service]:
66
- """Return the services registered in the extension."""
67
- _services = current_service_registry._services # type: ignore[attr-defined] # noqa: SLF001
68
- return cast("dict[str, Service]", _services)
69
-
70
- def get_record_service_for_record(self, record: Any) -> RecordService:
71
- """Retrieve the associated service for a given record."""
72
- if record is None:
73
- raise ValueError("Need to pass a record instance, got None")
74
- return self.get_record_service_for_record_class(type(record))
75
-
76
- def get_record_service_for_record_class(self, record_cls: type[RecordBase]) -> RecordService:
77
- """Retrieve the service associated with a given record class."""
78
- for t in record_cls.mro():
79
- if t is RecordBase:
80
- break
81
- if t in self.models_by_record_class:
82
- model = self.models_by_record_class[t]
83
- return model.service
84
- raise KeyError(f"No service found for record class '{record_cls.__name__}'.")