oarepo-runtime 2.0.0.dev3__py3-none-any.whl → 2.0.0.dev4__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.0dev3"
22
+ __version__ = "2.0.0dev4"
23
23
 
24
24
  __all__ = ("Model", "OARepoRuntime", "__version__", "current_runtime")
oarepo_runtime/api.py CHANGED
@@ -11,22 +11,62 @@
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ import dataclasses
15
+ from functools import cached_property
14
16
  from typing import TYPE_CHECKING, cast
15
17
 
18
+ from flask import current_app
19
+ from invenio_base.utils import obj_or_import_string
16
20
  from invenio_records_resources.proxies import current_service_registry
17
21
 
18
22
  if TYPE_CHECKING:
19
23
  from flask_babel.speaklater import LazyString
24
+ from flask_resources.responses import ResponseHandler
25
+ from flask_resources.serializers import BaseSerializer
20
26
  from invenio_drafts_resources.records.api import Draft
21
27
  from invenio_records_resources.records.api import RecordBase
28
+ from invenio_records_resources.resources.records.config import RecordResourceConfig
29
+ from invenio_records_resources.resources.records.resource import RecordResource
22
30
  from invenio_records_resources.services import RecordService, RecordServiceConfig
23
31
 
24
32
 
33
+ @dataclasses.dataclass
34
+ class Export:
35
+ """Configuration of an export format.
36
+
37
+ Exports are shown on the record landing page and user can download them.
38
+ """
39
+
40
+ name: LazyString
41
+ """Name of the export format, human readable."""
42
+
43
+ mimetype: str
44
+ """MIME type of the export format."""
45
+
46
+ serializer: BaseSerializer
47
+ """Serializer used to serialize the record into the export format."""
48
+
49
+ display: bool = True
50
+ """Whether the export format is displayed in the UI."""
51
+
52
+ oai_metadata_prefix: str | None = None
53
+ """OAI metadata prefix, if applicable. If not set, the export can not be used in OAI-PMH responses."""
54
+
55
+ oai_schema: str | None = None
56
+ """OAI schema, if applicable. If not set, the export can not be used in OAI-PMH responses."""
57
+
58
+ oai_namespace: str | None = None
59
+ """OAI namespace, if applicable. If not set, the export can not be used in OAI-PMH responses."""
60
+
61
+
25
62
  class Model[
26
63
  S: RecordService = RecordService,
27
64
  C: RecordServiceConfig = RecordServiceConfig,
28
65
  R: RecordBase = RecordBase,
29
66
  D: Draft = Draft,
67
+ # not sure why this is flagged by pyright as an error
68
+ RR: RecordResource = RecordResource, # pyright: ignore[reportGeneralTypeIssues]
69
+ RC: RecordResourceConfig = RecordResourceConfig,
30
70
  ]:
31
71
  """Model configuration.
32
72
 
@@ -36,21 +76,32 @@ class Model[
36
76
  """
37
77
 
38
78
  name: str | LazyString
79
+ """Name of the model, human readable."""
80
+
39
81
  version: str
82
+ """Version of the model, should be a valid semantic version."""
83
+
40
84
  description: str | LazyString | None = None
41
- global_search_enabled: bool = False
85
+ """Description of the model, human readable."""
86
+
87
+ records_alias_enabled: bool = False
88
+ """Whether the records alias is enabled for this model. Such models will be searchable
89
+ via the `/api/records` endpoint."""
42
90
 
43
91
  def __init__( # noqa: PLR0913 more attributes as we are creating a config
44
92
  self,
45
93
  name: str | LazyString,
46
94
  version: str,
47
95
  service: str | S,
96
+ resource_config: RC | str,
48
97
  # params with default values
49
98
  service_config: C | None = None,
50
99
  description: str | LazyString | None = None,
51
100
  record: type[R] | None = None,
52
101
  draft: type[D] | None = None,
53
- global_search_enabled: bool = True,
102
+ resource: str | RR = "invenio_records_resources.resources.records.resource.RecordResource",
103
+ exports: list[Export] | None = None,
104
+ records_alias_enabled: bool = True,
54
105
  ):
55
106
  """Initialize the model configuration.
56
107
 
@@ -65,17 +116,28 @@ class Model[
65
116
  configuration.
66
117
  :param draft: Draft class, if not provided, it will be taken from the service
67
118
  configuration.
119
+ :param resource: Resource class or string import path to the resource class.
120
+ If not provided, it will be taken from the service configuration.
121
+ :param resource_config: Resource configuration, if not provided, it will be
122
+ taken from the resource class.
123
+ :param exports: List of export formats that can be used to export the record.
124
+ If not provided, no exports are available.
125
+ :param records_alias_enabled: Whether the records alias is enabled for this model.
126
+ Such models will be searchable via the `/api/records` endpoint.
68
127
  """
69
128
  self.name = name
70
129
  self.version = version
71
130
  self.description = description
72
- self.global_search_enabled = global_search_enabled
131
+ self.records_alias_enabled = records_alias_enabled
73
132
 
74
133
  # lazy getters ...
75
134
  self._record = record
76
135
  self._draft = draft
77
136
  self._service = service
78
137
  self._service_config = service_config
138
+ self._resource = resource
139
+ self._resource_config = resource_config
140
+ self._exports = exports or []
79
141
 
80
142
  @property
81
143
  def service(self) -> S:
@@ -109,3 +171,40 @@ class Model[
109
171
  return cast("type[D]", self.service.config.draft_cls)
110
172
  return None
111
173
  return self._draft
174
+
175
+ @cached_property
176
+ def resource_config(self) -> RC:
177
+ """Get the resource configuration."""
178
+ if isinstance(self._resource_config, str):
179
+ resource_config_class: type[RC] = cast("type[RC]", obj_or_import_string(self._resource_config))
180
+ # need to import it here to avoid circular import issues
181
+ from .config import build_config
182
+
183
+ return build_config(resource_config_class, current_app)
184
+ return self._resource_config
185
+
186
+ @cached_property
187
+ def resource(self) -> RR:
188
+ """Get the resource."""
189
+ if isinstance(self._resource, str):
190
+ resource_class = obj_or_import_string(self._resource)
191
+ if resource_class is None:
192
+ raise ValueError(f"Resource class {self._resource} can not be None.")
193
+ return cast(
194
+ "RR",
195
+ resource_class(
196
+ service=self.service,
197
+ config=self.resource_config,
198
+ ),
199
+ )
200
+ return self._resource
201
+
202
+ @property
203
+ def exports(self) -> list[Export]:
204
+ """Get all exportable response handlers."""
205
+ return self._exports
206
+
207
+ @property
208
+ def response_handlers(self) -> dict[str, ResponseHandler]:
209
+ """Get all response handlers from the resource configuration."""
210
+ return cast("dict[str, ResponseHandler]", self.resource_config.response_handlers)
oarepo_runtime/config.py CHANGED
@@ -47,7 +47,9 @@ OAREPO_MODELS: dict[str, Model] = {
47
47
  version=vocabularies_version,
48
48
  service="vocabularies",
49
49
  description="Base (non-specialized) invenio vocabularies",
50
- global_search_enabled=False,
50
+ records_alias_enabled=False,
51
+ resource_config="invenio_vocabularies.resources.config.VocabulariesResourceConfig",
52
+ resource="invenio_vocabularies.resources.resource.VocabulariesResource",
51
53
  ),
52
54
  # affiliations
53
55
  "affiliations": Model(
@@ -55,7 +57,9 @@ OAREPO_MODELS: dict[str, Model] = {
55
57
  version=vocabularies_version,
56
58
  service="affiliations",
57
59
  description="Affiliations vocabulary",
58
- global_search_enabled=False,
60
+ records_alias_enabled=False,
61
+ resource_config="invenio_vocabularies.contrib.affiliations.resources.AffiliationsResourceConfig",
62
+ resource="invenio_vocabularies.contrib.affiliations.resources.AffiliationsResource",
59
63
  ),
60
64
  # funders
61
65
  "funders": Model(
@@ -63,7 +67,9 @@ OAREPO_MODELS: dict[str, Model] = {
63
67
  version=vocabularies_version,
64
68
  service="funders",
65
69
  description="Funders vocabulary",
66
- global_search_enabled=False,
70
+ records_alias_enabled=False,
71
+ resource_config="invenio_vocabularies.contrib.funders.resources.FundersResourceConfig",
72
+ resource="invenio_vocabularies.contrib.funders.resources.FundersResource",
67
73
  ),
68
74
  # awards
69
75
  "awards": Model(
@@ -71,7 +77,9 @@ OAREPO_MODELS: dict[str, Model] = {
71
77
  version=vocabularies_version,
72
78
  service="awards",
73
79
  description="Awards vocabulary",
74
- global_search_enabled=False,
80
+ records_alias_enabled=False,
81
+ resource_config="invenio_vocabularies.contrib.awards.resources.AwardsResourceConfig",
82
+ resource="invenio_vocabularies.contrib.awards.resources.AwardsResource",
75
83
  ),
76
84
  # names
77
85
  "names": Model(
@@ -79,7 +87,9 @@ OAREPO_MODELS: dict[str, Model] = {
79
87
  version=vocabularies_version,
80
88
  service="names",
81
89
  description="Names vocabulary",
82
- global_search_enabled=False,
90
+ records_alias_enabled=False,
91
+ resource_config="invenio_vocabularies.contrib.names.resources.NamesResourceConfig",
92
+ resource="invenio_vocabularies.contrib.names.resources.NamesResource",
83
93
  ),
84
94
  # subjects
85
95
  "subjects": Model(
@@ -87,6 +97,8 @@ OAREPO_MODELS: dict[str, Model] = {
87
97
  version=vocabularies_version,
88
98
  service="subjects",
89
99
  description="Subjects vocabulary",
90
- global_search_enabled=False,
100
+ records_alias_enabled=False,
101
+ resource_config="invenio_vocabularies.contrib.subjects.resources.SubjectsResourceConfig",
102
+ resource="invenio_vocabularies.contrib.subjects.resources.SubjectsResource",
91
103
  ),
92
104
  }
@@ -47,9 +47,11 @@ class PublicationStatusSystemField(MappingSystemFieldMixin, SystemField):
47
47
  @override
48
48
  def post_dump(self, record: RecordBase, data: dict, dumper: Dumper | None = None) -> None:
49
49
  if self.key is None:
50
- return
50
+ return # pragma: no cover
51
51
  if not self.attr_name:
52
- raise ValueError("attr_name must be set for PublicationStatusSystemField")
52
+ raise ValueError( # pragma: no cover
53
+ "attr_name must be set for PublicationStatusSystemField"
54
+ )
53
55
  data[self.key] = getattr(record, self.attr_name)
54
56
 
55
57
  def __get__(self, record: RecordBase | None, owner: Any = None) -> Any:
@@ -0,0 +1,12 @@
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
+ """Facet parameter classes."""
11
+
12
+ from __future__ import annotations
@@ -0,0 +1,127 @@
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
+ """Facet params."""
11
+
12
+ from __future__ import annotations
13
+
14
+ import copy
15
+ import logging
16
+ from typing import TYPE_CHECKING
17
+
18
+ from flask import current_app
19
+ from invenio_access.permissions import system_user_id
20
+ from invenio_app.helpers import obj_or_import_string
21
+ from invenio_records_resources.services.records.facets import FacetsResponse
22
+ from invenio_records_resources.services.records.params import FacetsParam
23
+
24
+ if TYPE_CHECKING:
25
+ from flask_principal import Identity
26
+ from invenio_records_resources.services.records.config import SearchOptions
27
+ from invenio_search.engine import dsl
28
+
29
+ log = logging.getLogger(__name__)
30
+
31
+
32
+ class GroupedFacetsParam(FacetsParam):
33
+ """Facet parameter class that supports grouping of facets."""
34
+
35
+ def __init__(self, config: SearchOptions):
36
+ """Initialize the facets parameter with the given config."""
37
+ super().__init__(config)
38
+ self._facets = {**config.facets}
39
+
40
+ @property
41
+ def facets(self) -> dict[str, dsl.Facet]:
42
+ """Return the facets dictionary."""
43
+ return self._facets
44
+
45
+ def identity_facet_groups(self, identity: Identity) -> list[str]:
46
+ """Return the facet groups for the given identity."""
47
+ if "OAREPO_FACET_GROUP_NAME" in current_app.config:
48
+ find_facet_groups_func = obj_or_import_string(current_app.config["OAREPO_FACET_GROUP_NAME"])
49
+ return find_facet_groups_func(identity, self.config, None) # type: ignore[no-any-return]
50
+
51
+ if hasattr(identity, "provides"):
52
+ return [need.value for need in identity.provides if need.method == "role"]
53
+
54
+ return []
55
+
56
+ @property
57
+ def facet_groups(self) -> dict[str, dict[str, dsl.Facet]] | None:
58
+ """Return the facet groups defined in the service config."""
59
+ if hasattr(self.config, "facet_groups"):
60
+ return self.config.facet_groups # type: ignore[no-any-return]
61
+ return None
62
+
63
+ def identity_facets(self, identity: Identity) -> dict[str, dsl.Facet]:
64
+ """Return the facets for the given identity."""
65
+ if not self.facet_groups:
66
+ return self.facets
67
+
68
+ has_system_user_id = identity.id == system_user_id
69
+ has_system_process_need = any(need.method == "system_process" for need in identity.provides)
70
+ if has_system_user_id or has_system_process_need:
71
+ return self.facets
72
+
73
+ return self._filter_user_facets(identity)
74
+
75
+ def aggregate_with_user_facets(self, search: dsl.Search, user_facets: dict[str, dsl.Facet]) -> dsl.Search:
76
+ """Add aggregations representing the user facets."""
77
+ for name, facet in user_facets.items():
78
+ agg = facet.get_aggregation()
79
+ search.aggs.bucket(name, agg)
80
+
81
+ return search
82
+
83
+ def filter(self, search: dsl.Search) -> dsl.Search:
84
+ """Apply a post filter on the search."""
85
+ if not self._filters:
86
+ return search
87
+
88
+ filters = list(self._filters.values())
89
+
90
+ _filter = filters[0]
91
+ for f in filters[1:]:
92
+ _filter &= f
93
+
94
+ return search.filter(_filter).post_filter(_filter)
95
+
96
+ def apply(self, identity: Identity, search: dsl.Search, params: dict) -> dsl.Search:
97
+ """Evaluate the facets on the search."""
98
+ facets_values = params.pop("facets", {})
99
+ for name, values in facets_values.items():
100
+ if name in self.facets:
101
+ self.add_filter(name, values)
102
+
103
+ user_facets = self.identity_facets(identity)
104
+ self_copy = copy.copy(self)
105
+ self_copy._facets = user_facets # noqa: SLF001 - TODO: this looks like a hack
106
+ search = search.response_class(FacetsResponse.create_response_cls(self_copy))
107
+
108
+ search = self.aggregate_with_user_facets(search, user_facets)
109
+ search = self.filter(search)
110
+
111
+ params.update(self.selected_values)
112
+
113
+ return search
114
+
115
+ def _filter_user_facets(self, identity: Identity) -> dict[str, dsl.Facet]:
116
+ """Filter user facets based on the identity."""
117
+ user_facets = {}
118
+ if not self.facet_groups:
119
+ user_facets.update(self.facets)
120
+ else:
121
+ self.facets.clear() # TODO: why is this needed?
122
+ user_facets.update(self.facet_groups.get("default", {}))
123
+
124
+ groups = self.identity_facet_groups(identity)
125
+ for group in groups:
126
+ user_facets.update((self.facet_groups or {}).get(group, {}))
127
+ return user_facets
@@ -1,7 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oarepo-runtime
3
- Version: 2.0.0.dev3
3
+ Version: 2.0.0.dev4
4
4
  Summary: A set of runtime extensions of Invenio repository
5
+ Project-URL: Homepage, https://github.com/oarepo/oarepo-runtime
5
6
  License-Expression: MIT
6
7
  License-File: LICENSE
7
8
  Requires-Python: <3.14,>=3.13
@@ -1,6 +1,6 @@
1
- oarepo_runtime/__init__.py,sha256=hLYRfE2IAzqCkcxlBoCopCZceljM89PTrpiN7k5FHv8,685
2
- oarepo_runtime/api.py,sha256=h3iwvDtL1AFYOQqKd6HJj1cnbxM_gy1IWt4gFcSzzSc,3826
3
- oarepo_runtime/config.py,sha256=auPBzCMXIcYdA1Tee5srMtfu0D5lDd38sn4BO5wGVJ4,2540
1
+ oarepo_runtime/__init__.py,sha256=powOkFU-kfVV4CnzBjvPV2cY4nCgQShIk8B9IE6t2Lc,685
2
+ oarepo_runtime/api.py,sha256=HfvfaocS-xmuEf7LqZICcGM84eivXDccXluppUmTw_w,7977
3
+ oarepo_runtime/config.py,sha256=6Cm5tUobpJrxJWgtLJnZcckeBeecrjdpwG59_bYqIqU,3620
4
4
  oarepo_runtime/ext.py,sha256=AMb5pMnCSbqIpPyP99YUKlf9vopz_b2ZW-RnvfsEVlk,3254
5
5
  oarepo_runtime/proxies.py,sha256=PXaRiBh5qs5-h8M81cJOgtqypFQcYUSjiSn2TLSujRw,648
6
6
  oarepo_runtime/cli/__init__.py,sha256=iPs1a4blP7750rwEXobzK3YHgsfGxoVTZxwWMslAlTY,350
@@ -11,20 +11,22 @@ oarepo_runtime/records/mapping.py,sha256=SJbSzerT1645a93-3-Fgz_i3anzFNlrZqbjjwW2
11
11
  oarepo_runtime/records/pid_providers.py,sha256=pVXVeYmAsXy-IEdM2zHZ7UWkAnzXg1gtssfLc9QZbPA,1717
12
12
  oarepo_runtime/records/systemfields/__init__.py,sha256=g-u408qyNnsbUTpDtVVwlcyiJaO68GTjDN0W9rXs9pk,524
13
13
  oarepo_runtime/records/systemfields/mapping.py,sha256=66OQavKewJEUMkghymOxvskIO0LUSP2E-MbHryeT5Nk,1968
14
- oarepo_runtime/records/systemfields/publication_status.py,sha256=vYIwueI37Auh84IptSxeF9KYC32EGmBe3-Rk1DWXMF8,1957
14
+ oarepo_runtime/records/systemfields/publication_status.py,sha256=1g3VXNPh0FsiPCpe-7ZuaMEF4x8ffrDrt37Rqnjp0ng,2027
15
15
  oarepo_runtime/services/__init__.py,sha256=OGtBgEeaDTyk2RPDNXuKbU9_7egFBZr42SM0gN5FrF4,341
16
16
  oarepo_runtime/services/results.py,sha256=fk-Enx_LwZLbw81yZ7CXVTku86vd3_fjprnb8l5sFHk,6657
17
17
  oarepo_runtime/services/config/__init__.py,sha256=SX1kfIGk8HkohdLQrNpRQUTltksEyDcCa-kFXxrX4e8,711
18
18
  oarepo_runtime/services/config/link_conditions.py,sha256=raqf4yaBNLqNYgBxVNblo8MRJneVIFkwVNW7IW3AVYI,4309
19
19
  oarepo_runtime/services/config/permissions.py,sha256=x5k61LGnpXyJfXVoCTq2tTVTtPckmBcBtcBJx4UN9EA,3056
20
+ oarepo_runtime/services/facets/__init__.py,sha256=k39ZYt1dMVOW01QRSTgx3CfuTYwvEWmL0VYTR3huVsE,349
21
+ oarepo_runtime/services/facets/params.py,sha256=B8RssdAt8bWRzwcTc5ireFkJg-q22DDOAprk9uhIwL4,4691
20
22
  oarepo_runtime/services/records/__init__.py,sha256=c0n4vcMcJhSUWsKz0iyV4TTlGG8oLlJp0YEN0QGCZ8U,428
21
23
  oarepo_runtime/services/records/links.py,sha256=lqvnXabquL4DqWV93cdUYNUVYHx7T5Q0EXXuWAHM33A,1078
22
24
  oarepo_runtime/services/records/mapping.py,sha256=y3oeToKEnaRYpMV3q2-2cXNzyzyL3XXGvY26BifybpE,1332
23
25
  oarepo_runtime/services/schema/__init__.py,sha256=jgAPI_uKC6Ug4KQWnwQVg3-aNaw-eHja323AUFo5ELo,351
24
26
  oarepo_runtime/services/schema/i18n.py,sha256=9D1zOQaPKAnYzejB0vO-m2BJYnam0N0Lrq4jID7twfE,3174
25
27
  oarepo_runtime/services/schema/i18n_ui.py,sha256=DbusphhGDeaobTt4nuwNgKZ6Houlu4Sv3SuMGkdjRRY,3582
26
- oarepo_runtime-2.0.0.dev3.dist-info/METADATA,sha256=nLtfS-x8LURACObTJYTyGeXIzHMQ9jtSzfg_l145cHo,4430
27
- oarepo_runtime-2.0.0.dev3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
- oarepo_runtime-2.0.0.dev3.dist-info/entry_points.txt,sha256=7HqK5jumIgDVJa7ifjPKizginfIm5_R_qUVKPf_Yq-c,145
29
- oarepo_runtime-2.0.0.dev3.dist-info/licenses/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
30
- oarepo_runtime-2.0.0.dev3.dist-info/RECORD,,
28
+ oarepo_runtime-2.0.0.dev4.dist-info/METADATA,sha256=bhUIFD18Rl7nvQtQGGljHi0MEnBrYxVoCsOa_eg-R_M,4494
29
+ oarepo_runtime-2.0.0.dev4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
+ oarepo_runtime-2.0.0.dev4.dist-info/entry_points.txt,sha256=7HqK5jumIgDVJa7ifjPKizginfIm5_R_qUVKPf_Yq-c,145
31
+ oarepo_runtime-2.0.0.dev4.dist-info/licenses/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
32
+ oarepo_runtime-2.0.0.dev4.dist-info/RECORD,,