oarepo-runtime 1.10.2__py3-none-any.whl → 2.0.0.dev3__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.
Files changed (171) hide show
  1. oarepo_runtime/__init__.py +24 -0
  2. oarepo_runtime/api.py +111 -0
  3. oarepo_runtime/cli/__init__.py +10 -21
  4. oarepo_runtime/cli/search.py +34 -0
  5. oarepo_runtime/config.py +86 -13
  6. oarepo_runtime/ext.py +64 -82
  7. oarepo_runtime/proxies.py +21 -5
  8. oarepo_runtime/records/__init__.py +11 -50
  9. oarepo_runtime/records/drafts.py +24 -18
  10. oarepo_runtime/records/mapping.py +84 -0
  11. oarepo_runtime/records/pid_providers.py +43 -7
  12. oarepo_runtime/records/systemfields/__init__.py +15 -33
  13. oarepo_runtime/records/systemfields/mapping.py +41 -24
  14. oarepo_runtime/records/systemfields/publication_status.py +59 -0
  15. oarepo_runtime/services/__init__.py +12 -0
  16. oarepo_runtime/services/config/__init__.py +15 -21
  17. oarepo_runtime/services/config/link_conditions.py +69 -75
  18. oarepo_runtime/services/config/permissions.py +62 -0
  19. oarepo_runtime/services/records/__init__.py +14 -1
  20. oarepo_runtime/services/records/links.py +21 -11
  21. oarepo_runtime/services/records/mapping.py +42 -0
  22. oarepo_runtime/services/results.py +98 -109
  23. oarepo_runtime/services/schema/__init__.py +12 -44
  24. oarepo_runtime/services/schema/i18n.py +47 -22
  25. oarepo_runtime/services/schema/i18n_ui.py +61 -24
  26. {oarepo_runtime-1.10.2.dist-info → oarepo_runtime-2.0.0.dev3.dist-info}/METADATA +9 -21
  27. oarepo_runtime-2.0.0.dev3.dist-info/RECORD +30 -0
  28. {oarepo_runtime-1.10.2.dist-info → oarepo_runtime-2.0.0.dev3.dist-info}/WHEEL +1 -2
  29. oarepo_runtime-2.0.0.dev3.dist-info/entry_points.txt +5 -0
  30. oarepo_runtime/cli/assets.py +0 -145
  31. oarepo_runtime/cli/base.py +0 -25
  32. oarepo_runtime/cli/cf.py +0 -15
  33. oarepo_runtime/cli/check.py +0 -167
  34. oarepo_runtime/cli/configuration.py +0 -51
  35. oarepo_runtime/cli/fixtures.py +0 -167
  36. oarepo_runtime/cli/index.py +0 -272
  37. oarepo_runtime/cli/permissions/__init__.py +0 -6
  38. oarepo_runtime/cli/permissions/base.py +0 -26
  39. oarepo_runtime/cli/permissions/evaluate.py +0 -63
  40. oarepo_runtime/cli/permissions/list.py +0 -239
  41. oarepo_runtime/cli/permissions/search.py +0 -121
  42. oarepo_runtime/cli/validate.py +0 -150
  43. oarepo_runtime/datastreams/__init__.py +0 -38
  44. oarepo_runtime/datastreams/asynchronous.py +0 -247
  45. oarepo_runtime/datastreams/catalogue.py +0 -150
  46. oarepo_runtime/datastreams/datastreams.py +0 -152
  47. oarepo_runtime/datastreams/errors.py +0 -54
  48. oarepo_runtime/datastreams/ext.py +0 -41
  49. oarepo_runtime/datastreams/fixtures.py +0 -265
  50. oarepo_runtime/datastreams/json.py +0 -4
  51. oarepo_runtime/datastreams/readers/__init__.py +0 -39
  52. oarepo_runtime/datastreams/readers/attachments.py +0 -51
  53. oarepo_runtime/datastreams/readers/excel.py +0 -123
  54. oarepo_runtime/datastreams/readers/json.py +0 -27
  55. oarepo_runtime/datastreams/readers/service.py +0 -54
  56. oarepo_runtime/datastreams/readers/yaml.py +0 -14
  57. oarepo_runtime/datastreams/semi_asynchronous.py +0 -91
  58. oarepo_runtime/datastreams/synchronous.py +0 -70
  59. oarepo_runtime/datastreams/transformers.py +0 -18
  60. oarepo_runtime/datastreams/types.py +0 -323
  61. oarepo_runtime/datastreams/utils.py +0 -131
  62. oarepo_runtime/datastreams/writers/__init__.py +0 -21
  63. oarepo_runtime/datastreams/writers/attachments_file.py +0 -92
  64. oarepo_runtime/datastreams/writers/attachments_service.py +0 -118
  65. oarepo_runtime/datastreams/writers/publish.py +0 -70
  66. oarepo_runtime/datastreams/writers/service.py +0 -175
  67. oarepo_runtime/datastreams/writers/utils.py +0 -30
  68. oarepo_runtime/datastreams/writers/validation_errors.py +0 -20
  69. oarepo_runtime/datastreams/writers/yaml.py +0 -56
  70. oarepo_runtime/ext_config.py +0 -67
  71. oarepo_runtime/i18n/__init__.py +0 -3
  72. oarepo_runtime/info/__init__.py +0 -0
  73. oarepo_runtime/info/check.py +0 -95
  74. oarepo_runtime/info/permissions/__init__.py +0 -0
  75. oarepo_runtime/info/permissions/debug.py +0 -191
  76. oarepo_runtime/info/views.py +0 -586
  77. oarepo_runtime/profile.py +0 -60
  78. oarepo_runtime/records/dumpers/__init__.py +0 -8
  79. oarepo_runtime/records/dumpers/edtf_interval.py +0 -38
  80. oarepo_runtime/records/dumpers/multilingual_dumper.py +0 -34
  81. oarepo_runtime/records/entity_resolvers/__init__.py +0 -13
  82. oarepo_runtime/records/entity_resolvers/proxies.py +0 -57
  83. oarepo_runtime/records/mappings/__init__.py +0 -0
  84. oarepo_runtime/records/mappings/rdm_parent_mapping.json +0 -483
  85. oarepo_runtime/records/owners/__init__.py +0 -3
  86. oarepo_runtime/records/owners/registry.py +0 -22
  87. oarepo_runtime/records/relations/__init__.py +0 -22
  88. oarepo_runtime/records/relations/base.py +0 -296
  89. oarepo_runtime/records/relations/internal.py +0 -46
  90. oarepo_runtime/records/relations/lookup.py +0 -28
  91. oarepo_runtime/records/relations/pid_relation.py +0 -102
  92. oarepo_runtime/records/systemfields/featured_file.py +0 -45
  93. oarepo_runtime/records/systemfields/has_draftcheck.py +0 -47
  94. oarepo_runtime/records/systemfields/icu.py +0 -371
  95. oarepo_runtime/records/systemfields/owner.py +0 -115
  96. oarepo_runtime/records/systemfields/record_status.py +0 -35
  97. oarepo_runtime/records/systemfields/selectors.py +0 -98
  98. oarepo_runtime/records/systemfields/synthetic.py +0 -130
  99. oarepo_runtime/resources/__init__.py +0 -4
  100. oarepo_runtime/resources/config.py +0 -12
  101. oarepo_runtime/resources/file_resource.py +0 -15
  102. oarepo_runtime/resources/json_serializer.py +0 -27
  103. oarepo_runtime/resources/localized_ui_json_serializer.py +0 -54
  104. oarepo_runtime/resources/resource.py +0 -53
  105. oarepo_runtime/resources/responses.py +0 -20
  106. oarepo_runtime/services/components.py +0 -429
  107. oarepo_runtime/services/config/draft_link.py +0 -23
  108. oarepo_runtime/services/config/permissions_presets.py +0 -174
  109. oarepo_runtime/services/config/service.py +0 -117
  110. oarepo_runtime/services/custom_fields/__init__.py +0 -80
  111. oarepo_runtime/services/custom_fields/mappings.py +0 -188
  112. oarepo_runtime/services/entity/__init__.py +0 -0
  113. oarepo_runtime/services/entity/config.py +0 -14
  114. oarepo_runtime/services/entity/schema.py +0 -9
  115. oarepo_runtime/services/entity/service.py +0 -48
  116. oarepo_runtime/services/expansions/__init__.py +0 -0
  117. oarepo_runtime/services/expansions/expandable_fields.py +0 -21
  118. oarepo_runtime/services/expansions/service.py +0 -4
  119. oarepo_runtime/services/facets/__init__.py +0 -33
  120. oarepo_runtime/services/facets/base.py +0 -12
  121. oarepo_runtime/services/facets/date.py +0 -72
  122. oarepo_runtime/services/facets/enum.py +0 -11
  123. oarepo_runtime/services/facets/facet_groups_names.py +0 -17
  124. oarepo_runtime/services/facets/max_facet.py +0 -13
  125. oarepo_runtime/services/facets/multilingual_facet.py +0 -33
  126. oarepo_runtime/services/facets/nested_facet.py +0 -32
  127. oarepo_runtime/services/facets/params.py +0 -192
  128. oarepo_runtime/services/facets/year_histogram.py +0 -200
  129. oarepo_runtime/services/files/__init__.py +0 -8
  130. oarepo_runtime/services/files/components.py +0 -62
  131. oarepo_runtime/services/files/service.py +0 -16
  132. oarepo_runtime/services/generators.py +0 -10
  133. oarepo_runtime/services/permissions/__init__.py +0 -3
  134. oarepo_runtime/services/permissions/generators.py +0 -103
  135. oarepo_runtime/services/relations/__init__.py +0 -0
  136. oarepo_runtime/services/relations/components.py +0 -15
  137. oarepo_runtime/services/relations/errors.py +0 -18
  138. oarepo_runtime/services/relations/mapping.py +0 -38
  139. oarepo_runtime/services/schema/cf.py +0 -13
  140. oarepo_runtime/services/schema/i18n_validation.py +0 -7
  141. oarepo_runtime/services/schema/marshmallow.py +0 -44
  142. oarepo_runtime/services/schema/marshmallow_to_json_schema.py +0 -72
  143. oarepo_runtime/services/schema/oneofschema.py +0 -192
  144. oarepo_runtime/services/schema/polymorphic.py +0 -21
  145. oarepo_runtime/services/schema/rdm.py +0 -75
  146. oarepo_runtime/services/schema/rdm_ui.py +0 -156
  147. oarepo_runtime/services/schema/ui.py +0 -251
  148. oarepo_runtime/services/schema/validation.py +0 -70
  149. oarepo_runtime/services/search.py +0 -282
  150. oarepo_runtime/services/service.py +0 -61
  151. oarepo_runtime/tasks.py +0 -6
  152. oarepo_runtime/translations/cs/LC_MESSAGES/messages.mo +0 -0
  153. oarepo_runtime/translations/cs/LC_MESSAGES/messages.po +0 -85
  154. oarepo_runtime/translations/default_translations.py +0 -6
  155. oarepo_runtime/translations/en/LC_MESSAGES/messages.mo +0 -0
  156. oarepo_runtime/translations/en/LC_MESSAGES/messages.po +0 -89
  157. oarepo_runtime/translations/messages.pot +0 -91
  158. oarepo_runtime/uow.py +0 -146
  159. oarepo_runtime/utils/__init__.py +0 -0
  160. oarepo_runtime/utils/functools.py +0 -37
  161. oarepo_runtime/utils/identity_utils.py +0 -35
  162. oarepo_runtime/utils/index.py +0 -11
  163. oarepo_runtime/utils/path.py +0 -97
  164. oarepo_runtime-1.10.2.dist-info/RECORD +0 -163
  165. oarepo_runtime-1.10.2.dist-info/entry_points.txt +0 -16
  166. oarepo_runtime-1.10.2.dist-info/top_level.txt +0 -2
  167. tests/marshmallow_to_json/__init__.py +0 -0
  168. tests/marshmallow_to_json/test_datacite_ui_schema.py +0 -1410
  169. tests/marshmallow_to_json/test_simple_schema.py +0 -52
  170. tests/pkg_data/__init__.py +0 -0
  171. {oarepo_runtime-1.10.2.dist-info → oarepo_runtime-2.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
@@ -1,3 +1,16 @@
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 records module."""
11
+
12
+ from __future__ import annotations
13
+
1
14
  from .links import pagination_links_html
2
15
 
3
- __all__ = ("pagination_links_html",)
16
+ __all__ = ("pagination_links_html",)
@@ -1,21 +1,31 @@
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
+ """Utility for rendering URI template links."""
11
+
12
+ from __future__ import annotations
13
+
1
14
  from invenio_records_resources.services.base.links import Link
2
15
 
3
- def pagination_links_html(tpl: str)->dict[str, Link]:
4
- """Create pagination links (prev/selv/next) from the same template."""
16
+
17
+ def pagination_links_html(tpl: str) -> dict[str, Link]:
18
+ """Create pagination links (prev/self/next) from the same template."""
5
19
  return {
6
20
  "prev_html": Link(
7
21
  tpl,
8
- when=lambda pagination, ctx: pagination.has_prev,
9
- vars=lambda pagination, vars: vars["args"].update(
10
- {"page": pagination.prev_page.page}
11
- ),
22
+ when=lambda pagination, _context: pagination.has_prev,
23
+ vars=lambda pagination, variables: variables["args"].update({"page": pagination.prev_page.page}),
12
24
  ),
13
25
  "self_html": Link(tpl),
14
26
  "next_html": Link(
15
27
  tpl,
16
- when=lambda pagination, ctx: pagination.has_next,
17
- vars=lambda pagination, vars: vars["args"].update(
18
- {"page": pagination.next_page.page}
19
- ),
28
+ when=lambda pagination, _context: pagination.has_next,
29
+ vars=lambda pagination, variables: variables["args"].update({"page": pagination.next_page.page}),
20
30
  ),
21
- }
31
+ }
@@ -0,0 +1,42 @@
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
+ """Update mappings for all record classes in the service registry."""
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ from invenio_records_resources.services.records import (
16
+ RecordService,
17
+ RecordServiceConfig,
18
+ )
19
+
20
+ from oarepo_runtime import current_runtime
21
+ from oarepo_runtime.records.mapping import update_record_system_fields_mapping
22
+
23
+ if TYPE_CHECKING:
24
+ from invenio_records_resources.services.base import Service
25
+
26
+
27
+ def update_all_records_mappings() -> None:
28
+ """Update all mappings for the registered record classes."""
29
+ service: Service
30
+ for service in current_runtime.services.values():
31
+ if not isinstance(service, RecordService):
32
+ continue
33
+
34
+ config: RecordServiceConfig = service.config
35
+
36
+ record_class = getattr(config, "record_cls", None)
37
+ if record_class:
38
+ update_record_system_fields_mapping(record_class)
39
+
40
+ draft_class = getattr(config, "draft_cls", None)
41
+ if draft_class:
42
+ update_record_system_fields_mapping(draft_class)
@@ -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 ResultsComponent:
15
- def update_data(self, identity, record, projection, expand):
16
- raise NotImplementedError
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
- return postprocess_errors(self._errors)
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
- def postprocess_error_messages(field_path: str, messages: any):
51
- """Postprocess error messages, looking for those that were not correctly processed by marshmallow/invenio."""
52
- if not isinstance(messages, list):
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
- converted_errors.append(error)
75
- return converted_errors
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
- components = []
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
- return result
100
- except AttributeError:
101
- return None
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 hit_dict.get("record_status") == "draft":
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=dict(
120
- identity=self._identity,
121
- record=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
- self._service.config.links_search_item
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
- elif self._links_item_tpl:
130
- projection["links"] = self._links_item_tpl.expand(
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
- from .polymorphic import PolymorphicSchema
2
-
3
-
4
- def consistent_resolution(*classes):
5
- """
6
- A helper function to solve resolution order of classes.
7
- If the classes are in a correct mro order, it will return
8
- them in the same order. Otherwise it will try to reorder
9
- them and remove those that are already contained in mro
10
- of others.
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, value_name, value_field="marshmallow_utils.fields.SanitizedHTML"
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
- if value != "_" and not langcodes.Language.get(value).is_valid():
22
- raise ValidationError("Invalid language code")
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, **kwargs):
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),