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.
Files changed (171) hide show
  1. oarepo_runtime/__init__.py +24 -0
  2. oarepo_runtime/api.py +210 -0
  3. oarepo_runtime/cli/__init__.py +10 -21
  4. oarepo_runtime/cli/search.py +34 -0
  5. oarepo_runtime/config.py +98 -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 +61 -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/facets/__init__.py +12 -33
  20. oarepo_runtime/services/facets/params.py +45 -110
  21. oarepo_runtime/services/records/__init__.py +14 -1
  22. oarepo_runtime/services/records/links.py +21 -11
  23. oarepo_runtime/services/records/mapping.py +42 -0
  24. oarepo_runtime/services/results.py +98 -109
  25. oarepo_runtime/services/schema/__init__.py +12 -44
  26. oarepo_runtime/services/schema/i18n.py +47 -22
  27. oarepo_runtime/services/schema/i18n_ui.py +61 -24
  28. {oarepo_runtime-1.10.3.dist-info → oarepo_runtime-2.0.0.dev4.dist-info}/METADATA +10 -21
  29. oarepo_runtime-2.0.0.dev4.dist-info/RECORD +32 -0
  30. {oarepo_runtime-1.10.3.dist-info → oarepo_runtime-2.0.0.dev4.dist-info}/WHEEL +1 -2
  31. oarepo_runtime-2.0.0.dev4.dist-info/entry_points.txt +5 -0
  32. oarepo_runtime/cli/assets.py +0 -145
  33. oarepo_runtime/cli/base.py +0 -25
  34. oarepo_runtime/cli/cf.py +0 -15
  35. oarepo_runtime/cli/check.py +0 -167
  36. oarepo_runtime/cli/configuration.py +0 -51
  37. oarepo_runtime/cli/fixtures.py +0 -167
  38. oarepo_runtime/cli/index.py +0 -272
  39. oarepo_runtime/cli/permissions/__init__.py +0 -6
  40. oarepo_runtime/cli/permissions/base.py +0 -26
  41. oarepo_runtime/cli/permissions/evaluate.py +0 -63
  42. oarepo_runtime/cli/permissions/list.py +0 -239
  43. oarepo_runtime/cli/permissions/search.py +0 -121
  44. oarepo_runtime/cli/validate.py +0 -150
  45. oarepo_runtime/datastreams/__init__.py +0 -38
  46. oarepo_runtime/datastreams/asynchronous.py +0 -247
  47. oarepo_runtime/datastreams/catalogue.py +0 -150
  48. oarepo_runtime/datastreams/datastreams.py +0 -152
  49. oarepo_runtime/datastreams/errors.py +0 -54
  50. oarepo_runtime/datastreams/ext.py +0 -41
  51. oarepo_runtime/datastreams/fixtures.py +0 -265
  52. oarepo_runtime/datastreams/json.py +0 -4
  53. oarepo_runtime/datastreams/readers/__init__.py +0 -39
  54. oarepo_runtime/datastreams/readers/attachments.py +0 -51
  55. oarepo_runtime/datastreams/readers/excel.py +0 -123
  56. oarepo_runtime/datastreams/readers/json.py +0 -27
  57. oarepo_runtime/datastreams/readers/service.py +0 -54
  58. oarepo_runtime/datastreams/readers/yaml.py +0 -14
  59. oarepo_runtime/datastreams/semi_asynchronous.py +0 -91
  60. oarepo_runtime/datastreams/synchronous.py +0 -70
  61. oarepo_runtime/datastreams/transformers.py +0 -18
  62. oarepo_runtime/datastreams/types.py +0 -323
  63. oarepo_runtime/datastreams/utils.py +0 -131
  64. oarepo_runtime/datastreams/writers/__init__.py +0 -21
  65. oarepo_runtime/datastreams/writers/attachments_file.py +0 -92
  66. oarepo_runtime/datastreams/writers/attachments_service.py +0 -118
  67. oarepo_runtime/datastreams/writers/publish.py +0 -70
  68. oarepo_runtime/datastreams/writers/service.py +0 -175
  69. oarepo_runtime/datastreams/writers/utils.py +0 -30
  70. oarepo_runtime/datastreams/writers/validation_errors.py +0 -20
  71. oarepo_runtime/datastreams/writers/yaml.py +0 -56
  72. oarepo_runtime/ext_config.py +0 -67
  73. oarepo_runtime/i18n/__init__.py +0 -3
  74. oarepo_runtime/info/__init__.py +0 -0
  75. oarepo_runtime/info/check.py +0 -95
  76. oarepo_runtime/info/permissions/__init__.py +0 -0
  77. oarepo_runtime/info/permissions/debug.py +0 -191
  78. oarepo_runtime/info/views.py +0 -586
  79. oarepo_runtime/profile.py +0 -60
  80. oarepo_runtime/records/dumpers/__init__.py +0 -8
  81. oarepo_runtime/records/dumpers/edtf_interval.py +0 -38
  82. oarepo_runtime/records/dumpers/multilingual_dumper.py +0 -34
  83. oarepo_runtime/records/entity_resolvers/__init__.py +0 -13
  84. oarepo_runtime/records/entity_resolvers/proxies.py +0 -57
  85. oarepo_runtime/records/mappings/__init__.py +0 -0
  86. oarepo_runtime/records/mappings/rdm_parent_mapping.json +0 -483
  87. oarepo_runtime/records/owners/__init__.py +0 -3
  88. oarepo_runtime/records/owners/registry.py +0 -22
  89. oarepo_runtime/records/relations/__init__.py +0 -22
  90. oarepo_runtime/records/relations/base.py +0 -296
  91. oarepo_runtime/records/relations/internal.py +0 -46
  92. oarepo_runtime/records/relations/lookup.py +0 -28
  93. oarepo_runtime/records/relations/pid_relation.py +0 -102
  94. oarepo_runtime/records/systemfields/featured_file.py +0 -45
  95. oarepo_runtime/records/systemfields/has_draftcheck.py +0 -47
  96. oarepo_runtime/records/systemfields/icu.py +0 -371
  97. oarepo_runtime/records/systemfields/owner.py +0 -115
  98. oarepo_runtime/records/systemfields/record_status.py +0 -35
  99. oarepo_runtime/records/systemfields/selectors.py +0 -98
  100. oarepo_runtime/records/systemfields/synthetic.py +0 -130
  101. oarepo_runtime/resources/__init__.py +0 -4
  102. oarepo_runtime/resources/config.py +0 -12
  103. oarepo_runtime/resources/file_resource.py +0 -15
  104. oarepo_runtime/resources/json_serializer.py +0 -27
  105. oarepo_runtime/resources/localized_ui_json_serializer.py +0 -54
  106. oarepo_runtime/resources/resource.py +0 -53
  107. oarepo_runtime/resources/responses.py +0 -20
  108. oarepo_runtime/services/components.py +0 -429
  109. oarepo_runtime/services/config/draft_link.py +0 -23
  110. oarepo_runtime/services/config/permissions_presets.py +0 -174
  111. oarepo_runtime/services/config/service.py +0 -117
  112. oarepo_runtime/services/custom_fields/__init__.py +0 -80
  113. oarepo_runtime/services/custom_fields/mappings.py +0 -188
  114. oarepo_runtime/services/entity/__init__.py +0 -0
  115. oarepo_runtime/services/entity/config.py +0 -14
  116. oarepo_runtime/services/entity/schema.py +0 -9
  117. oarepo_runtime/services/entity/service.py +0 -48
  118. oarepo_runtime/services/expansions/__init__.py +0 -0
  119. oarepo_runtime/services/expansions/expandable_fields.py +0 -21
  120. oarepo_runtime/services/expansions/service.py +0 -4
  121. oarepo_runtime/services/facets/base.py +0 -12
  122. oarepo_runtime/services/facets/date.py +0 -72
  123. oarepo_runtime/services/facets/enum.py +0 -11
  124. oarepo_runtime/services/facets/facet_groups_names.py +0 -17
  125. oarepo_runtime/services/facets/max_facet.py +0 -13
  126. oarepo_runtime/services/facets/multilingual_facet.py +0 -33
  127. oarepo_runtime/services/facets/nested_facet.py +0 -32
  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 -146
  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 -95
  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 -97
  157. oarepo_runtime/translations/messages.pot +0 -100
  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.3.dist-info/RECORD +0 -163
  165. oarepo_runtime-1.10.3.dist-info/entry_points.txt +0 -16
  166. oarepo_runtime-1.10.3.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.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 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),
@@ -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, value_name, value_field="marshmallow_utils.fields.SanitizedHTML"
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, value, attr=None, obj=None, **kwargs):
53
- if not value:
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 value:
93
+ for v in obj:
57
94
  if language == v[lang_name]:
58
95
  return v[value_name]
59
- return next(iter(value))[value_name]
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: 1.10.3
3
+ Version: 2.0.0.dev4
4
4
  Summary: A set of runtime extensions of Invenio repository
5
- Description-Content-Type: text/markdown
5
+ Project-URL: Homepage, https://github.com/oarepo/oarepo-runtime
6
+ License-Expression: MIT
6
7
  License-File: LICENSE
7
- Requires-Dist: marshmallow
8
- Requires-Dist: langcodes
9
- Requires-Dist: pyyaml
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 == "dev"
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 == "tests"
24
- Requires-Dist: pytest-oarepo; extra == "tests"
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,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-