oarepo-runtime 1.10.3__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.
- oarepo_runtime/__init__.py +24 -0
- oarepo_runtime/api.py +210 -0
- oarepo_runtime/cli/__init__.py +10 -21
- oarepo_runtime/cli/search.py +34 -0
- oarepo_runtime/config.py +98 -13
- oarepo_runtime/ext.py +64 -82
- oarepo_runtime/proxies.py +21 -5
- oarepo_runtime/records/__init__.py +11 -50
- oarepo_runtime/records/drafts.py +24 -18
- oarepo_runtime/records/mapping.py +84 -0
- oarepo_runtime/records/pid_providers.py +43 -7
- oarepo_runtime/records/systemfields/__init__.py +15 -33
- oarepo_runtime/records/systemfields/mapping.py +41 -24
- oarepo_runtime/records/systemfields/publication_status.py +61 -0
- oarepo_runtime/services/__init__.py +12 -0
- oarepo_runtime/services/config/__init__.py +15 -21
- oarepo_runtime/services/config/link_conditions.py +69 -75
- oarepo_runtime/services/config/permissions.py +62 -0
- oarepo_runtime/services/facets/__init__.py +12 -33
- oarepo_runtime/services/facets/params.py +45 -110
- oarepo_runtime/services/records/__init__.py +14 -1
- oarepo_runtime/services/records/links.py +21 -11
- oarepo_runtime/services/records/mapping.py +42 -0
- oarepo_runtime/services/results.py +98 -109
- oarepo_runtime/services/schema/__init__.py +12 -44
- oarepo_runtime/services/schema/i18n.py +47 -22
- oarepo_runtime/services/schema/i18n_ui.py +61 -24
- {oarepo_runtime-1.10.3.dist-info → oarepo_runtime-2.0.0.dev4.dist-info}/METADATA +10 -21
- oarepo_runtime-2.0.0.dev4.dist-info/RECORD +32 -0
- {oarepo_runtime-1.10.3.dist-info → oarepo_runtime-2.0.0.dev4.dist-info}/WHEEL +1 -2
- oarepo_runtime-2.0.0.dev4.dist-info/entry_points.txt +5 -0
- oarepo_runtime/cli/assets.py +0 -145
- oarepo_runtime/cli/base.py +0 -25
- oarepo_runtime/cli/cf.py +0 -15
- oarepo_runtime/cli/check.py +0 -167
- oarepo_runtime/cli/configuration.py +0 -51
- oarepo_runtime/cli/fixtures.py +0 -167
- oarepo_runtime/cli/index.py +0 -272
- oarepo_runtime/cli/permissions/__init__.py +0 -6
- oarepo_runtime/cli/permissions/base.py +0 -26
- oarepo_runtime/cli/permissions/evaluate.py +0 -63
- oarepo_runtime/cli/permissions/list.py +0 -239
- oarepo_runtime/cli/permissions/search.py +0 -121
- oarepo_runtime/cli/validate.py +0 -150
- oarepo_runtime/datastreams/__init__.py +0 -38
- oarepo_runtime/datastreams/asynchronous.py +0 -247
- oarepo_runtime/datastreams/catalogue.py +0 -150
- oarepo_runtime/datastreams/datastreams.py +0 -152
- oarepo_runtime/datastreams/errors.py +0 -54
- oarepo_runtime/datastreams/ext.py +0 -41
- oarepo_runtime/datastreams/fixtures.py +0 -265
- oarepo_runtime/datastreams/json.py +0 -4
- oarepo_runtime/datastreams/readers/__init__.py +0 -39
- oarepo_runtime/datastreams/readers/attachments.py +0 -51
- oarepo_runtime/datastreams/readers/excel.py +0 -123
- oarepo_runtime/datastreams/readers/json.py +0 -27
- oarepo_runtime/datastreams/readers/service.py +0 -54
- oarepo_runtime/datastreams/readers/yaml.py +0 -14
- oarepo_runtime/datastreams/semi_asynchronous.py +0 -91
- oarepo_runtime/datastreams/synchronous.py +0 -70
- oarepo_runtime/datastreams/transformers.py +0 -18
- oarepo_runtime/datastreams/types.py +0 -323
- oarepo_runtime/datastreams/utils.py +0 -131
- oarepo_runtime/datastreams/writers/__init__.py +0 -21
- oarepo_runtime/datastreams/writers/attachments_file.py +0 -92
- oarepo_runtime/datastreams/writers/attachments_service.py +0 -118
- oarepo_runtime/datastreams/writers/publish.py +0 -70
- oarepo_runtime/datastreams/writers/service.py +0 -175
- oarepo_runtime/datastreams/writers/utils.py +0 -30
- oarepo_runtime/datastreams/writers/validation_errors.py +0 -20
- oarepo_runtime/datastreams/writers/yaml.py +0 -56
- oarepo_runtime/ext_config.py +0 -67
- oarepo_runtime/i18n/__init__.py +0 -3
- oarepo_runtime/info/__init__.py +0 -0
- oarepo_runtime/info/check.py +0 -95
- oarepo_runtime/info/permissions/__init__.py +0 -0
- oarepo_runtime/info/permissions/debug.py +0 -191
- oarepo_runtime/info/views.py +0 -586
- oarepo_runtime/profile.py +0 -60
- oarepo_runtime/records/dumpers/__init__.py +0 -8
- oarepo_runtime/records/dumpers/edtf_interval.py +0 -38
- oarepo_runtime/records/dumpers/multilingual_dumper.py +0 -34
- oarepo_runtime/records/entity_resolvers/__init__.py +0 -13
- oarepo_runtime/records/entity_resolvers/proxies.py +0 -57
- oarepo_runtime/records/mappings/__init__.py +0 -0
- oarepo_runtime/records/mappings/rdm_parent_mapping.json +0 -483
- oarepo_runtime/records/owners/__init__.py +0 -3
- oarepo_runtime/records/owners/registry.py +0 -22
- oarepo_runtime/records/relations/__init__.py +0 -22
- oarepo_runtime/records/relations/base.py +0 -296
- oarepo_runtime/records/relations/internal.py +0 -46
- oarepo_runtime/records/relations/lookup.py +0 -28
- oarepo_runtime/records/relations/pid_relation.py +0 -102
- oarepo_runtime/records/systemfields/featured_file.py +0 -45
- oarepo_runtime/records/systemfields/has_draftcheck.py +0 -47
- oarepo_runtime/records/systemfields/icu.py +0 -371
- oarepo_runtime/records/systemfields/owner.py +0 -115
- oarepo_runtime/records/systemfields/record_status.py +0 -35
- oarepo_runtime/records/systemfields/selectors.py +0 -98
- oarepo_runtime/records/systemfields/synthetic.py +0 -130
- oarepo_runtime/resources/__init__.py +0 -4
- oarepo_runtime/resources/config.py +0 -12
- oarepo_runtime/resources/file_resource.py +0 -15
- oarepo_runtime/resources/json_serializer.py +0 -27
- oarepo_runtime/resources/localized_ui_json_serializer.py +0 -54
- oarepo_runtime/resources/resource.py +0 -53
- oarepo_runtime/resources/responses.py +0 -20
- oarepo_runtime/services/components.py +0 -429
- oarepo_runtime/services/config/draft_link.py +0 -23
- oarepo_runtime/services/config/permissions_presets.py +0 -174
- oarepo_runtime/services/config/service.py +0 -117
- oarepo_runtime/services/custom_fields/__init__.py +0 -80
- oarepo_runtime/services/custom_fields/mappings.py +0 -188
- oarepo_runtime/services/entity/__init__.py +0 -0
- oarepo_runtime/services/entity/config.py +0 -14
- oarepo_runtime/services/entity/schema.py +0 -9
- oarepo_runtime/services/entity/service.py +0 -48
- oarepo_runtime/services/expansions/__init__.py +0 -0
- oarepo_runtime/services/expansions/expandable_fields.py +0 -21
- oarepo_runtime/services/expansions/service.py +0 -4
- oarepo_runtime/services/facets/base.py +0 -12
- oarepo_runtime/services/facets/date.py +0 -72
- oarepo_runtime/services/facets/enum.py +0 -11
- oarepo_runtime/services/facets/facet_groups_names.py +0 -17
- oarepo_runtime/services/facets/max_facet.py +0 -13
- oarepo_runtime/services/facets/multilingual_facet.py +0 -33
- oarepo_runtime/services/facets/nested_facet.py +0 -32
- oarepo_runtime/services/facets/year_histogram.py +0 -200
- oarepo_runtime/services/files/__init__.py +0 -8
- oarepo_runtime/services/files/components.py +0 -62
- oarepo_runtime/services/files/service.py +0 -16
- oarepo_runtime/services/generators.py +0 -10
- oarepo_runtime/services/permissions/__init__.py +0 -3
- oarepo_runtime/services/permissions/generators.py +0 -103
- oarepo_runtime/services/relations/__init__.py +0 -0
- oarepo_runtime/services/relations/components.py +0 -15
- oarepo_runtime/services/relations/errors.py +0 -18
- oarepo_runtime/services/relations/mapping.py +0 -38
- oarepo_runtime/services/schema/cf.py +0 -13
- oarepo_runtime/services/schema/i18n_validation.py +0 -7
- oarepo_runtime/services/schema/marshmallow.py +0 -44
- oarepo_runtime/services/schema/marshmallow_to_json_schema.py +0 -72
- oarepo_runtime/services/schema/oneofschema.py +0 -192
- oarepo_runtime/services/schema/polymorphic.py +0 -21
- oarepo_runtime/services/schema/rdm.py +0 -146
- oarepo_runtime/services/schema/rdm_ui.py +0 -156
- oarepo_runtime/services/schema/ui.py +0 -251
- oarepo_runtime/services/schema/validation.py +0 -70
- oarepo_runtime/services/search.py +0 -282
- oarepo_runtime/services/service.py +0 -61
- oarepo_runtime/tasks.py +0 -6
- oarepo_runtime/translations/cs/LC_MESSAGES/messages.mo +0 -0
- oarepo_runtime/translations/cs/LC_MESSAGES/messages.po +0 -95
- oarepo_runtime/translations/default_translations.py +0 -6
- oarepo_runtime/translations/en/LC_MESSAGES/messages.mo +0 -0
- oarepo_runtime/translations/en/LC_MESSAGES/messages.po +0 -97
- oarepo_runtime/translations/messages.pot +0 -100
- oarepo_runtime/uow.py +0 -146
- oarepo_runtime/utils/__init__.py +0 -0
- oarepo_runtime/utils/functools.py +0 -37
- oarepo_runtime/utils/identity_utils.py +0 -35
- oarepo_runtime/utils/index.py +0 -11
- oarepo_runtime/utils/path.py +0 -97
- oarepo_runtime-1.10.3.dist-info/RECORD +0 -163
- oarepo_runtime-1.10.3.dist-info/entry_points.txt +0 -16
- oarepo_runtime-1.10.3.dist-info/top_level.txt +0 -2
- tests/marshmallow_to_json/__init__.py +0 -0
- tests/marshmallow_to_json/test_datacite_ui_schema.py +0 -1410
- tests/marshmallow_to_json/test_simple_schema.py +0 -52
- tests/pkg_data/__init__.py +0 -0
- {oarepo_runtime-1.10.3.dist-info → oarepo_runtime-2.0.0.dev4.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,21 @@
|
|
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
|
+
"""Service results."""
|
11
|
+
|
12
|
+
from __future__ import annotations
|
13
|
+
|
1
14
|
import logging
|
15
|
+
from typing import TYPE_CHECKING, Any
|
2
16
|
|
17
|
+
from invenio_access.permissions import Identity
|
18
|
+
from invenio_records.api import RecordBase
|
3
19
|
from invenio_records_resources.errors import _iter_errors_dict
|
4
20
|
from invenio_records_resources.services.records.results import (
|
5
21
|
RecordItem as BaseRecordItem,
|
@@ -8,26 +24,50 @@ from invenio_records_resources.services.records.results import (
|
|
8
24
|
RecordList as BaseRecordList,
|
9
25
|
)
|
10
26
|
|
27
|
+
if TYPE_CHECKING:
|
28
|
+
from invenio_access.permissions import Identity
|
29
|
+
from invenio_records.api import RecordBase
|
30
|
+
|
11
31
|
log = logging.getLogger(__name__)
|
12
32
|
|
13
33
|
|
14
|
-
class
|
15
|
-
|
16
|
-
|
34
|
+
class ResultComponent:
|
35
|
+
"""Base class for result components that can modify the serialized record data."""
|
36
|
+
|
37
|
+
def __init__(
|
38
|
+
self,
|
39
|
+
record_item: BaseRecordItem | None = None,
|
40
|
+
record_list: BaseRecordList | None = None,
|
41
|
+
):
|
42
|
+
"""Initialize the result component."""
|
43
|
+
self._record_item = record_item
|
44
|
+
self._record_list = record_list
|
45
|
+
|
46
|
+
def update_data(self, identity: Identity, record: RecordBase, projection: dict, expand: bool) -> None:
|
47
|
+
"""Update the projection data with additional information.
|
48
|
+
|
49
|
+
:param identity: The identity of the user making the request.
|
50
|
+
:param record: The record being processed.
|
51
|
+
:param projection: The current projection of the record.
|
52
|
+
:param expand: Whether to expand the record data.
|
53
|
+
"""
|
54
|
+
raise NotImplementedError # pragma: no cover
|
17
55
|
|
18
56
|
|
19
57
|
class RecordItem(BaseRecordItem):
|
20
58
|
"""Single record result."""
|
21
59
|
|
22
|
-
components =
|
60
|
+
components: tuple[type[ResultComponent], ...] = ()
|
61
|
+
"""A list of components that can modify the serialized record data."""
|
23
62
|
|
24
63
|
@property
|
25
|
-
def data(self):
|
64
|
+
def data(self) -> Any:
|
65
|
+
"""Property to get the record."""
|
26
66
|
if self._data:
|
27
67
|
return self._data
|
28
68
|
_data = super().data
|
29
69
|
for c in self.components:
|
30
|
-
c.update_data(
|
70
|
+
c(record_item=self).update_data(
|
31
71
|
identity=self._identity,
|
32
72
|
record=self._record,
|
33
73
|
projection=_data,
|
@@ -36,57 +76,55 @@ class RecordItem(BaseRecordItem):
|
|
36
76
|
return _data
|
37
77
|
|
38
78
|
@property
|
39
|
-
def errors(self):
|
40
|
-
|
79
|
+
def errors(self) -> list[dict]:
|
80
|
+
"""Get the processed errors."""
|
81
|
+
return self.postprocess_errors(self._errors or [])
|
41
82
|
|
42
|
-
def to_dict(self):
|
83
|
+
def to_dict(self) -> Any:
|
43
84
|
"""Get a dictionary for the record."""
|
44
85
|
res = self.data
|
45
86
|
if self._errors:
|
46
87
|
res["errors"] = self.errors
|
47
88
|
return res
|
48
89
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
yield {"field": field_path, "messages": messages}
|
54
|
-
else:
|
55
|
-
str_messages = [msg for msg in messages if isinstance(msg, str)]
|
56
|
-
non_str_messages = [msg for msg in messages if not isinstance(msg, str)]
|
57
|
-
|
58
|
-
if str_messages:
|
59
|
-
yield {"field": field_path, "messages": str_messages}
|
60
|
-
else:
|
61
|
-
for non_str_msg in non_str_messages:
|
62
|
-
yield from _iter_errors_dict(non_str_msg, field_path)
|
63
|
-
|
64
|
-
|
65
|
-
def postprocess_errors(errors: list[dict]):
|
66
|
-
"""Postprocess errors."""
|
67
|
-
converted_errors = []
|
68
|
-
for error in errors:
|
69
|
-
if error.get("messages"):
|
70
|
-
converted_errors.extend(
|
71
|
-
postprocess_error_messages(error["field"], error["messages"])
|
72
|
-
)
|
90
|
+
def postprocess_error_messages(self, field_path: str, messages: Any) -> Any:
|
91
|
+
"""Postprocess error messages, looking for those that were not correctly processed by marshmallow/invenio."""
|
92
|
+
if not isinstance(messages, list):
|
93
|
+
yield {"field": field_path, "messages": messages}
|
73
94
|
else:
|
74
|
-
|
75
|
-
|
95
|
+
str_messages = [msg for msg in messages if isinstance(msg, str)]
|
96
|
+
non_str_messages = [msg for msg in messages if not isinstance(msg, str)]
|
97
|
+
|
98
|
+
if str_messages:
|
99
|
+
yield {"field": field_path, "messages": str_messages}
|
100
|
+
else:
|
101
|
+
for non_str_msg in non_str_messages:
|
102
|
+
yield from _iter_errors_dict(non_str_msg, field_path)
|
103
|
+
|
104
|
+
def postprocess_errors(self, errors: list[dict]) -> list[dict]:
|
105
|
+
"""Postprocess errors."""
|
106
|
+
converted_errors = []
|
107
|
+
for error in errors:
|
108
|
+
if error.get("messages"):
|
109
|
+
converted_errors.extend(self.postprocess_error_messages(error["field"], error["messages"]))
|
110
|
+
else:
|
111
|
+
converted_errors.append(error)
|
112
|
+
return converted_errors
|
76
113
|
|
77
114
|
|
78
115
|
class RecordList(BaseRecordList):
|
79
|
-
|
116
|
+
"""List of records result."""
|
117
|
+
|
118
|
+
components: tuple[type[ResultComponent], ...] = ()
|
80
119
|
|
81
120
|
@property
|
82
|
-
def aggregations(self):
|
121
|
+
def aggregations(self) -> Any:
|
83
122
|
"""Get the search result aggregations."""
|
84
123
|
try:
|
85
124
|
result = super().aggregations
|
86
125
|
if result is None:
|
87
|
-
return result
|
88
|
-
|
89
|
-
for key in result.keys():
|
126
|
+
return result # pragma: no cover
|
127
|
+
for key in result:
|
90
128
|
if "buckets" in result[key]:
|
91
129
|
for bucket in result[key]["buckets"]:
|
92
130
|
val = bucket["key"]
|
@@ -96,12 +134,12 @@ class RecordList(BaseRecordList):
|
|
96
134
|
bucket["key"] = str(val)
|
97
135
|
if not isinstance(label, str):
|
98
136
|
bucket["label"] = str(label)
|
99
|
-
|
100
|
-
|
101
|
-
|
137
|
+
except AttributeError: # pragma: no cover
|
138
|
+
return None # pragma: no cover
|
139
|
+
return result
|
102
140
|
|
103
141
|
@property
|
104
|
-
def hits(self):
|
142
|
+
def hits(self) -> Any:
|
105
143
|
"""Iterator over the hits."""
|
106
144
|
for hit in self._results:
|
107
145
|
# Load dump
|
@@ -109,87 +147,38 @@ class RecordList(BaseRecordList):
|
|
109
147
|
|
110
148
|
try:
|
111
149
|
# Project the record
|
112
|
-
if
|
150
|
+
# TODO: check if this logic is correct
|
151
|
+
versions = hit_dict.get("versions", {})
|
152
|
+
if versions.get("is_latest_draft") and not versions.get("is_latest"):
|
113
153
|
record = self._service.draft_cls.loads(hit_dict)
|
114
154
|
else:
|
115
155
|
record = self._service.record_cls.loads(hit_dict)
|
116
156
|
|
117
157
|
projection = self._schema.dump(
|
118
158
|
record,
|
119
|
-
context=
|
120
|
-
identity
|
121
|
-
record
|
122
|
-
|
159
|
+
context={
|
160
|
+
"identity": self._identity,
|
161
|
+
"record": record,
|
162
|
+
},
|
123
163
|
)
|
124
164
|
if hasattr(self._service.config, "links_search_item"):
|
125
|
-
links_tpl = self._service.config.search_item_links_template(
|
126
|
-
|
127
|
-
|
165
|
+
links_tpl = self._service.config.search_item_links_template(self._service.config.links_search_item)
|
166
|
+
else:
|
167
|
+
links_tpl = self._links_item_tpl
|
168
|
+
|
169
|
+
if links_tpl:
|
128
170
|
projection["links"] = links_tpl.expand(self._identity, record)
|
129
|
-
|
130
|
-
|
131
|
-
self._identity, record
|
132
|
-
)
|
133
|
-
# todo optimization viz FieldsResolver
|
171
|
+
|
172
|
+
# TODO: optimization viz FieldsResolver
|
134
173
|
for c in self.components:
|
135
|
-
c.update_data(
|
174
|
+
c(record_list=self).update_data(
|
136
175
|
identity=self._identity,
|
137
176
|
record=record,
|
138
177
|
projection=projection,
|
139
178
|
expand=self._expand,
|
140
179
|
)
|
141
180
|
yield projection
|
142
|
-
except Exception:
|
181
|
+
except Exception: # pragma: no cover
|
143
182
|
# ignore record with error, put it to log so that it gets to glitchtip
|
144
183
|
# but don't break the whole search
|
145
184
|
log.exception("Error while dumping record %s", hit_dict)
|
146
|
-
|
147
|
-
|
148
|
-
class ArrayRecordItem(RecordItem):
|
149
|
-
"""Single record result."""
|
150
|
-
|
151
|
-
@property
|
152
|
-
def id(self):
|
153
|
-
"""Get the record id."""
|
154
|
-
return self._record["id"]
|
155
|
-
|
156
|
-
|
157
|
-
class ArrayRecordList(RecordList):
|
158
|
-
# move to runtime
|
159
|
-
|
160
|
-
@property
|
161
|
-
def total(self):
|
162
|
-
"""Get total number of hits."""
|
163
|
-
return len(self._results)
|
164
|
-
|
165
|
-
@property
|
166
|
-
def aggregations(self):
|
167
|
-
"""Get the search result aggregations."""
|
168
|
-
return None
|
169
|
-
|
170
|
-
@property
|
171
|
-
def hits(self):
|
172
|
-
"""Iterator over the hits."""
|
173
|
-
for hit in self._results:
|
174
|
-
# Project the record
|
175
|
-
projection = self._schema.dump(
|
176
|
-
hit,
|
177
|
-
context=dict(
|
178
|
-
identity=self._identity,
|
179
|
-
record=hit,
|
180
|
-
),
|
181
|
-
)
|
182
|
-
if self._links_item_tpl:
|
183
|
-
projection["links"] = self._links_item_tpl.expand(self._identity, hit)
|
184
|
-
if self._nested_links_item:
|
185
|
-
for link in self._nested_links_item:
|
186
|
-
link.expand(self._identity, hit, projection)
|
187
|
-
|
188
|
-
for c in self.components:
|
189
|
-
c.update_data(
|
190
|
-
identity=self._identity,
|
191
|
-
record=hit,
|
192
|
-
projection=projection,
|
193
|
-
expand=self._expand,
|
194
|
-
)
|
195
|
-
yield projection
|
@@ -1,44 +1,12 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
# remove classes that are already in mro of others
|
14
|
-
filtered_classes = []
|
15
|
-
for cls in classes:
|
16
|
-
for other_cls in classes:
|
17
|
-
if cls != other_cls and issubclass(other_cls, cls):
|
18
|
-
break
|
19
|
-
else:
|
20
|
-
if cls not in filtered_classes:
|
21
|
-
filtered_classes.append(cls)
|
22
|
-
|
23
|
-
name = [cls.__name__ for cls in filtered_classes]
|
24
|
-
name = "".join(name) + "ConsistentResolution"
|
25
|
-
try:
|
26
|
-
return type(name, tuple(filtered_classes), {})
|
27
|
-
except TypeError:
|
28
|
-
pass
|
29
|
-
|
30
|
-
filtered_classes.sort(key=lambda cls: -len(cls.mro()))
|
31
|
-
try:
|
32
|
-
return type(name, tuple(filtered_classes), {})
|
33
|
-
except TypeError:
|
34
|
-
pass
|
35
|
-
|
36
|
-
bases = ", ".join(cls.__name__ for cls in filtered_classes)
|
37
|
-
orig_bases = ", ".join(cls.__name__ for cls in classes)
|
38
|
-
raise TypeError(
|
39
|
-
f"Cannot create a consistent method resolution order (MRO) "
|
40
|
-
f"for bases {orig_bases}, tried {bases}"
|
41
|
-
)
|
42
|
-
|
43
|
-
|
44
|
-
__all__ = ("PolymorphicSchema", "consistent_resolution")
|
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
|
+
"""Marshmallow schema module."""
|
11
|
+
|
12
|
+
from __future__ import annotations
|
@@ -1,28 +1,48 @@
|
|
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
|
+
"""Marshmallow schema for multilingual strings."""
|
11
|
+
|
12
|
+
from __future__ import annotations
|
13
|
+
|
1
14
|
from functools import lru_cache
|
15
|
+
from typing import Any
|
2
16
|
|
3
17
|
import langcodes
|
18
|
+
import langcodes.tag_parser
|
4
19
|
from invenio_base.utils import obj_or_import_string
|
5
20
|
from invenio_i18n import gettext as _
|
6
21
|
from marshmallow import Schema, ValidationError, fields, pre_load, validates
|
7
22
|
|
8
|
-
"""
|
9
|
-
Marshmallow schema for multilingual strings. Consider moving this file to a library, not generating
|
10
|
-
it for each project.
|
11
|
-
"""
|
12
|
-
|
13
23
|
|
14
24
|
@lru_cache
|
15
25
|
def get_i18n_schema(
|
16
|
-
lang_name
|
17
|
-
|
26
|
+
lang_name: str,
|
27
|
+
value_name: str,
|
28
|
+
value_field: str = "marshmallow_utils.fields.SanitizedHTML",
|
29
|
+
) -> type[Schema]:
|
30
|
+
"""Dynamically creates and returns I18n Schema class.
|
31
|
+
|
32
|
+
Add custom serialization logic based on the provided `lang_name` and `value_name`.
|
33
|
+
"""
|
34
|
+
|
18
35
|
class I18nMixin:
|
19
36
|
@validates(lang_name)
|
20
|
-
def validate_lang(self, value):
|
21
|
-
|
22
|
-
|
37
|
+
def validate_lang(self, value: str) -> None:
|
38
|
+
try:
|
39
|
+
if value != "_" and not langcodes.Language.get(value).is_valid():
|
40
|
+
raise ValidationError("Invalid language code")
|
41
|
+
except langcodes.tag_parser.LanguageTagError as e:
|
42
|
+
raise ValidationError("Invalid language code") from e
|
23
43
|
|
24
44
|
@pre_load
|
25
|
-
def pre_load_func(self, data, **
|
45
|
+
def pre_load_func(self, data: dict[str, Any], **_kwargs: Any) -> dict[str, Any]:
|
26
46
|
errors = {}
|
27
47
|
if not data.get(lang_name) or not data.get(value_name):
|
28
48
|
errors[lang_name] = [_("Both language and text must be provided.")]
|
@@ -33,7 +53,11 @@ def get_i18n_schema(
|
|
33
53
|
return data
|
34
54
|
|
35
55
|
value_field_class = obj_or_import_string(value_field)
|
36
|
-
|
56
|
+
if value_field_class is None:
|
57
|
+
raise ValueError(
|
58
|
+
f"Invalid value field class provided: '{value_field}'. "
|
59
|
+
"Expected a valid import string for a Marshmallow field class."
|
60
|
+
)
|
37
61
|
return type(
|
38
62
|
f"I18nSchema_{lang_name}_{value_name}",
|
39
63
|
(
|
@@ -48,14 +72,15 @@ def get_i18n_schema(
|
|
48
72
|
|
49
73
|
|
50
74
|
def MultilingualField( # noqa NOSONAR
|
51
|
-
*args,
|
52
|
-
lang_name="lang",
|
53
|
-
value_name="value",
|
54
|
-
value_field="marshmallow_utils.fields.SanitizedHTML",
|
55
|
-
**kwargs,
|
75
|
+
*args: Any,
|
76
|
+
lang_name: str = "lang",
|
77
|
+
value_name: str = "value",
|
78
|
+
value_field: str = "marshmallow_utils.fields.SanitizedHTML",
|
79
|
+
**kwargs: Any,
|
56
80
|
):
|
57
81
|
# TODO: args are not used but oarepo-model-builder-multilingual generates them
|
58
82
|
# should be fixed there and subsequently removed here
|
83
|
+
_ = args
|
59
84
|
return fields.List(
|
60
85
|
fields.Nested(get_i18n_schema(lang_name, value_name, value_field)),
|
61
86
|
**kwargs,
|
@@ -63,11 +88,11 @@ def MultilingualField( # noqa NOSONAR
|
|
63
88
|
|
64
89
|
|
65
90
|
def I18nStrField( # noqa NOSONAR
|
66
|
-
*args,
|
67
|
-
lang_name="lang",
|
68
|
-
value_name="value",
|
69
|
-
value_field="marshmallow_utils.fields.SanitizedHTML",
|
70
|
-
**kwargs,
|
91
|
+
*args: Any,
|
92
|
+
lang_name: str = "lang",
|
93
|
+
value_name: str = "value",
|
94
|
+
value_field: str = "marshmallow_utils.fields.SanitizedHTML",
|
95
|
+
**kwargs: Any,
|
71
96
|
):
|
72
97
|
return fields.Nested(
|
73
98
|
get_i18n_schema(lang_name, value_name, value_field),
|
@@ -1,4 +1,23 @@
|
|
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
|
+
"""Functionality for handling multilingual UI schemas using Marshmallow.
|
11
|
+
|
12
|
+
It includes dynamic schema generation based on
|
13
|
+
the provided language and value names, as well as specialized fields for
|
14
|
+
working with multilingual data and localized user interfaces.
|
15
|
+
"""
|
16
|
+
|
17
|
+
from __future__ import annotations
|
18
|
+
|
1
19
|
from functools import lru_cache
|
20
|
+
from typing import Any
|
2
21
|
|
3
22
|
from invenio_base.utils import obj_or_import_string
|
4
23
|
from marshmallow import Schema, fields
|
@@ -6,9 +25,20 @@ from marshmallow import Schema, fields
|
|
6
25
|
|
7
26
|
@lru_cache
|
8
27
|
def get_i18n_ui_schema(
|
9
|
-
lang_name
|
10
|
-
|
28
|
+
lang_name: str,
|
29
|
+
value_name: str,
|
30
|
+
value_field: str = "marshmallow_utils.fields.SanitizedHTML",
|
31
|
+
) -> type[Schema]:
|
32
|
+
"""Dynamically creates and returns I18n Schema class.
|
33
|
+
|
34
|
+
Add custom serialization logic based on the provided `lang_name` and `value_name`.
|
35
|
+
"""
|
11
36
|
value_field_class = obj_or_import_string(value_field)
|
37
|
+
if value_field_class is None:
|
38
|
+
raise ValueError(
|
39
|
+
f"Invalid value field class provided: '{value_field}'. "
|
40
|
+
"Expected a valid import string for a Marshmallow field class."
|
41
|
+
)
|
12
42
|
return type(
|
13
43
|
f"I18nUISchema_{lang_name}_{value_name}",
|
14
44
|
(Schema,),
|
@@ -20,12 +50,13 @@ def get_i18n_ui_schema(
|
|
20
50
|
|
21
51
|
|
22
52
|
def MultilingualUIField( # noqa NOSONAR
|
23
|
-
*args,
|
24
|
-
lang_name="lang",
|
25
|
-
value_name="value",
|
26
|
-
value_field="marshmallow_utils.fields.SanitizedHTML",
|
27
|
-
**kwargs,
|
53
|
+
*args: Any,
|
54
|
+
lang_name: str = "lang",
|
55
|
+
value_name: str = "value",
|
56
|
+
value_field: str = "marshmallow_utils.fields.SanitizedHTML",
|
57
|
+
**kwargs: Any,
|
28
58
|
):
|
59
|
+
_ = args
|
29
60
|
return fields.List(
|
30
61
|
fields.Nested(get_i18n_ui_schema(lang_name, value_name, value_field)),
|
31
62
|
**kwargs,
|
@@ -33,12 +64,12 @@ def MultilingualUIField( # noqa NOSONAR
|
|
33
64
|
|
34
65
|
|
35
66
|
def I18nStrUIField( # noqa NOSONAR
|
36
|
-
*args,
|
37
|
-
lang_name="lang",
|
38
|
-
value_name="value",
|
39
|
-
value_field="marshmallow_utils.fields.SanitizedHTML",
|
40
|
-
**kwargs,
|
41
|
-
):
|
67
|
+
*args: Any,
|
68
|
+
lang_name: str = "lang",
|
69
|
+
value_name: str = "value",
|
70
|
+
value_field: str = "marshmallow_utils.fields.SanitizedHTML",
|
71
|
+
**kwargs: Any,
|
72
|
+
) -> fields.Field:
|
42
73
|
return fields.Nested(
|
43
74
|
get_i18n_ui_schema(lang_name, value_name, value_field),
|
44
75
|
*args,
|
@@ -47,16 +78,22 @@ def I18nStrUIField( # noqa NOSONAR
|
|
47
78
|
|
48
79
|
|
49
80
|
@lru_cache
|
50
|
-
def get_i18n_localized_ui_schema(lang_name, value_name):
|
81
|
+
def get_i18n_localized_ui_schema(lang_name: str, value_name: str) -> type[Schema]:
|
82
|
+
"""Dynamically creates and returns Localized I18n Schema class.
|
83
|
+
|
84
|
+
Add custom serialization logic based on the provided `lang_name` and `value_name`.
|
85
|
+
"""
|
86
|
+
|
51
87
|
class I18nLocalizedUISchema(Schema):
|
52
|
-
def _serialize(self,
|
53
|
-
|
88
|
+
def _serialize(self, obj: Any, *, many: bool | None = None) -> Any:
|
89
|
+
_ = many
|
90
|
+
if not obj:
|
54
91
|
return None
|
55
92
|
language = self.context["locale"].language
|
56
|
-
for v in
|
93
|
+
for v in obj:
|
57
94
|
if language == v[lang_name]:
|
58
95
|
return v[value_name]
|
59
|
-
return next(iter(
|
96
|
+
return next(iter(obj))[value_name]
|
60
97
|
|
61
98
|
# inherit to get a nice name for debugging
|
62
99
|
return type(
|
@@ -67,16 +104,16 @@ def get_i18n_localized_ui_schema(lang_name, value_name):
|
|
67
104
|
|
68
105
|
|
69
106
|
def MultilingualLocalizedUIField( # noqa NOSONAR
|
70
|
-
*args, lang_name="lang", value_name="value", **kwargs
|
71
|
-
):
|
72
|
-
return fields.Nested(get_i18n_localized_ui_schema(lang_name, value_name), **kwargs)
|
107
|
+
*args: Any, lang_name: str = "lang", value_name: str = "value", **kwargs: Any
|
108
|
+
) -> fields.Field:
|
109
|
+
return fields.Nested(get_i18n_localized_ui_schema(lang_name, value_name), *args, **kwargs)
|
73
110
|
|
74
111
|
|
75
112
|
def I18nStrLocalizedUIField( # noqa NOSONAR
|
76
|
-
*args, lang_name="lang", value_name="value", **kwargs
|
77
|
-
):
|
113
|
+
*args: Any, lang_name: str = "lang", value_name: str = "value", **kwargs: Any
|
114
|
+
) -> fields.Field:
|
115
|
+
_ = args
|
78
116
|
return fields.Nested(
|
79
117
|
get_i18n_ui_schema(lang_name, value_name),
|
80
|
-
*args,
|
81
118
|
**kwargs,
|
82
119
|
)
|
@@ -1,29 +1,18 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: oarepo-runtime
|
3
|
-
Version:
|
3
|
+
Version: 2.0.0.dev4
|
4
4
|
Summary: A set of runtime extensions of Invenio repository
|
5
|
-
|
5
|
+
Project-URL: Homepage, https://github.com/oarepo/oarepo-runtime
|
6
|
+
License-Expression: MIT
|
6
7
|
License-File: LICENSE
|
7
|
-
Requires-
|
8
|
-
Requires-Dist: langcodes
|
9
|
-
Requires-Dist:
|
10
|
-
Requires-Dist: openpyxl
|
11
|
-
Requires-Dist: flask-babelex
|
12
|
-
Requires-Dist: deepmerge
|
13
|
-
Requires-Dist: tqdm
|
14
|
-
Requires-Dist: deprecated
|
15
|
-
Requires-Dist: idutils
|
8
|
+
Requires-Python: <3.14,>=3.13
|
9
|
+
Requires-Dist: langcodes>=3.5.0
|
10
|
+
Requires-Dist: oarepo[rdm,tests]<14,>=13
|
16
11
|
Provides-Extra: dev
|
17
|
-
Requires-Dist: pytest>=7.1.2; extra ==
|
18
|
-
Requires-Dist: black; extra == "dev"
|
19
|
-
Requires-Dist: isort; extra == "dev"
|
20
|
-
Requires-Dist: autoflake; extra == "dev"
|
21
|
-
Requires-Dist: oarepo-tools; extra == "dev"
|
12
|
+
Requires-Dist: pytest>=7.1.2; extra == 'dev'
|
22
13
|
Provides-Extra: tests
|
23
|
-
Requires-Dist: pytest>=7.1.2; extra ==
|
24
|
-
|
25
|
-
Requires-Dist: psutil; extra == "tests"
|
26
|
-
Dynamic: license-file
|
14
|
+
Requires-Dist: pytest>=7.1.2; extra == 'tests'
|
15
|
+
Description-Content-Type: text/markdown
|
27
16
|
|
28
17
|
# OARepo runtime
|
29
18
|
|
@@ -172,4 +161,4 @@ Will print/write to file a json with the following format:
|
|
172
161
|
```
|
173
162
|
```json
|
174
163
|
|
175
|
-
```
|
164
|
+
```
|
@@ -0,0 +1,32 @@
|
|
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
|
+
oarepo_runtime/ext.py,sha256=AMb5pMnCSbqIpPyP99YUKlf9vopz_b2ZW-RnvfsEVlk,3254
|
5
|
+
oarepo_runtime/proxies.py,sha256=PXaRiBh5qs5-h8M81cJOgtqypFQcYUSjiSn2TLSujRw,648
|
6
|
+
oarepo_runtime/cli/__init__.py,sha256=iPs1a4blP7750rwEXobzK3YHgsfGxoVTZxwWMslAlTY,350
|
7
|
+
oarepo_runtime/cli/search.py,sha256=yqYHZauXsDBPpN4odYsPOWNQ9xWmAofQ407EAyqx6CY,1137
|
8
|
+
oarepo_runtime/records/__init__.py,sha256=AbWzmVCY7MhrpdEeI0e3lKzeugPMUSo8T08-NBVeig4,339
|
9
|
+
oarepo_runtime/records/drafts.py,sha256=CS-dUkrylNwscgBGfDyhwGBRCzwsyT6AA3Mhu40ShbY,1607
|
10
|
+
oarepo_runtime/records/mapping.py,sha256=SJbSzerT1645a93-3-Fgz_i3anzFNlrZqbjjwW2ctKs,2660
|
11
|
+
oarepo_runtime/records/pid_providers.py,sha256=pVXVeYmAsXy-IEdM2zHZ7UWkAnzXg1gtssfLc9QZbPA,1717
|
12
|
+
oarepo_runtime/records/systemfields/__init__.py,sha256=g-u408qyNnsbUTpDtVVwlcyiJaO68GTjDN0W9rXs9pk,524
|
13
|
+
oarepo_runtime/records/systemfields/mapping.py,sha256=66OQavKewJEUMkghymOxvskIO0LUSP2E-MbHryeT5Nk,1968
|
14
|
+
oarepo_runtime/records/systemfields/publication_status.py,sha256=1g3VXNPh0FsiPCpe-7ZuaMEF4x8ffrDrt37Rqnjp0ng,2027
|
15
|
+
oarepo_runtime/services/__init__.py,sha256=OGtBgEeaDTyk2RPDNXuKbU9_7egFBZr42SM0gN5FrF4,341
|
16
|
+
oarepo_runtime/services/results.py,sha256=fk-Enx_LwZLbw81yZ7CXVTku86vd3_fjprnb8l5sFHk,6657
|
17
|
+
oarepo_runtime/services/config/__init__.py,sha256=SX1kfIGk8HkohdLQrNpRQUTltksEyDcCa-kFXxrX4e8,711
|
18
|
+
oarepo_runtime/services/config/link_conditions.py,sha256=raqf4yaBNLqNYgBxVNblo8MRJneVIFkwVNW7IW3AVYI,4309
|
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
|
22
|
+
oarepo_runtime/services/records/__init__.py,sha256=c0n4vcMcJhSUWsKz0iyV4TTlGG8oLlJp0YEN0QGCZ8U,428
|
23
|
+
oarepo_runtime/services/records/links.py,sha256=lqvnXabquL4DqWV93cdUYNUVYHx7T5Q0EXXuWAHM33A,1078
|
24
|
+
oarepo_runtime/services/records/mapping.py,sha256=y3oeToKEnaRYpMV3q2-2cXNzyzyL3XXGvY26BifybpE,1332
|
25
|
+
oarepo_runtime/services/schema/__init__.py,sha256=jgAPI_uKC6Ug4KQWnwQVg3-aNaw-eHja323AUFo5ELo,351
|
26
|
+
oarepo_runtime/services/schema/i18n.py,sha256=9D1zOQaPKAnYzejB0vO-m2BJYnam0N0Lrq4jID7twfE,3174
|
27
|
+
oarepo_runtime/services/schema/i18n_ui.py,sha256=DbusphhGDeaobTt4nuwNgKZ6Houlu4Sv3SuMGkdjRRY,3582
|
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,,
|