oarepo-runtime 2.0.0.dev43__tar.gz → 2.0.0.dev45__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/PKG-INFO +1 -1
  2. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/__init__.py +1 -1
  3. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/api.py +25 -0
  4. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/ext.py +116 -1
  5. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/resources/signposting/__init__.py +17 -16
  6. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/.gitignore +0 -0
  7. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/LICENSE +0 -0
  8. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/README.md +0 -0
  9. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/cli/__init__.py +0 -0
  10. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/cli/search.py +0 -0
  11. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/config.py +0 -0
  12. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/info/__init__.py +0 -0
  13. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/info/views.py +0 -0
  14. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/proxies.py +0 -0
  15. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/py.typed +0 -0
  16. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/records/__init__.py +0 -0
  17. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/records/drafts.py +0 -0
  18. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/records/mapping.py +0 -0
  19. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/records/pid_providers.py +0 -0
  20. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/records/systemfields/__init__.py +0 -0
  21. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/records/systemfields/base.py +0 -0
  22. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/records/systemfields/custom_fields.py +0 -0
  23. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/records/systemfields/mapping.py +0 -0
  24. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/records/systemfields/publication_status.py +0 -0
  25. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/records/systemfields/relations.py +0 -0
  26. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/records/systemfields/selectors.py +0 -0
  27. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/resources/__init__.py +0 -0
  28. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/resources/config.py +0 -0
  29. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/__init__.py +0 -0
  30. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/config/__init__.py +0 -0
  31. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/config/components.py +0 -0
  32. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/config/link_conditions.py +0 -0
  33. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/config/permissions.py +0 -0
  34. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/facets/__init__.py +0 -0
  35. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/facets/base.py +0 -0
  36. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/facets/date.py +0 -0
  37. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/facets/nested_facet.py +0 -0
  38. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/facets/params.py +0 -0
  39. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/facets/utils.py +0 -0
  40. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/generators.py +0 -0
  41. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/records/__init__.py +0 -0
  42. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/records/custom_fields.py +0 -0
  43. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/records/links.py +0 -0
  44. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/records/mapping.py +0 -0
  45. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/results.py +0 -0
  46. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/schema/__init__.py +0 -0
  47. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/schema/i18n.py +0 -0
  48. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/schema/i18n_ui.py +0 -0
  49. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/services/schema/ui.py +0 -0
  50. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/oarepo_runtime/typing.py +0 -0
  51. {oarepo_runtime-2.0.0.dev43 → oarepo_runtime-2.0.0.dev45}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oarepo-runtime
3
- Version: 2.0.0.dev43
3
+ Version: 2.0.0.dev45
4
4
  Summary: A set of runtime extensions of Invenio repository
5
5
  Project-URL: Homepage, https://github.com/oarepo/oarepo-runtime
6
6
  License-Expression: MIT
@@ -19,6 +19,6 @@ from .api import Model
19
19
  from .ext import OARepoRuntime
20
20
  from .proxies import current_runtime
21
21
 
22
- __version__ = "2.0.0dev43"
22
+ __version__ = "2.0.0dev45"
23
23
 
24
24
  __all__ = ("Model", "OARepoRuntime", "__version__", "current_runtime")
@@ -13,6 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  import dataclasses
15
15
  from functools import cached_property
16
+ from mimetypes import guess_extension
16
17
  from typing import TYPE_CHECKING, Any, cast
17
18
 
18
19
  from flask import current_app
@@ -93,6 +94,23 @@ class Export:
93
94
  description: LazyString | None = None
94
95
  """Description of the export format, human readable."""
95
96
 
97
+ extension: str | None = None
98
+ """Extension of the export format, used in the filename when downloading the export."""
99
+
100
+ def __post_init__(self):
101
+ """Post init with extension guessing."""
102
+ if self.extension is None:
103
+ extension = guess_extension(self.mimetype)
104
+ if not extension:
105
+ first, second = self.mimetype.rsplit("/", maxsplit=1)
106
+ second = second.rsplit("+", maxsplit=1)[-1]
107
+ mimetype = f"{first}/{second}"
108
+ if mimetype != self.mimetype:
109
+ extension = guess_extension(mimetype)
110
+ self.extension = extension
111
+ if not self.extension:
112
+ self.extension = ".bin"
113
+
96
114
 
97
115
  @dataclasses.dataclass
98
116
  class Import:
@@ -399,6 +417,13 @@ class Model[
399
417
  return export
400
418
  return None
401
419
 
420
+ def get_export_by_code(self, code: str) -> Export | None:
421
+ """Get an export by code."""
422
+ for export in self._exports:
423
+ if export.code == code:
424
+ return export
425
+ return None
426
+
402
427
  @property
403
428
  def response_handlers(self) -> dict[str, ResponseHandler]:
404
429
  """Get all response handlers from the resource configuration."""
@@ -11,8 +11,10 @@
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ import json
15
+ from enum import Enum
14
16
  from functools import cached_property
15
- from typing import TYPE_CHECKING, Any, cast
17
+ from typing import TYPE_CHECKING, Any, Literal, cast, overload
16
18
 
17
19
  from flask import current_app
18
20
  from invenio_db import db
@@ -20,6 +22,7 @@ from invenio_pidstore.errors import PIDDoesNotExistError
20
22
  from invenio_pidstore.models import PersistentIdentifier
21
23
  from invenio_records.api import Record as RecordBase
22
24
  from invenio_records_resources.proxies import current_service_registry
25
+ from lxml.etree import fromstring
23
26
 
24
27
  from . import config
25
28
 
@@ -34,10 +37,19 @@ if TYPE_CHECKING: # pragma: no cover
34
37
  from invenio_records_resources.services.base.service import Service
35
38
  from invenio_records_resources.services.files.service import FileService
36
39
  from invenio_records_resources.services.records import RecordService
40
+ from lxml.etree import Element
37
41
 
38
42
  from .api import Model
39
43
 
40
44
 
45
+ class ExportRepresentation(Enum):
46
+ """Representation of the export, which can be response, dictionary or XML."""
47
+
48
+ RESPONSE = ("response",) # Response
49
+ DICTIONARY = ("dictionary",) # python dictionary
50
+ XML = ("xml",) # XML Element
51
+
52
+
41
53
  class OARepoRuntime:
42
54
  """OARepo base of invenio oarepo client."""
43
55
 
@@ -226,3 +238,106 @@ class OARepoRuntime:
226
238
  if draft_index is not None:
227
239
  indices.add(draft_index.search_alias)
228
240
  return indices
241
+
242
+ @overload
243
+ def get_export_from_serialized_record(
244
+ self,
245
+ record_dict: dict,
246
+ representation: Literal[ExportRepresentation.RESPONSE],
247
+ export_code: str | None = None,
248
+ export_mimetype: str | None = None,
249
+ ) -> tuple[Any, int, dict[str, str]] | None: ...
250
+
251
+ @overload
252
+ def get_export_from_serialized_record(
253
+ self,
254
+ record_dict: dict,
255
+ representation: Literal[ExportRepresentation.DICTIONARY],
256
+ export_code: str | None = None,
257
+ export_mimetype: str | None = None,
258
+ ) -> dict: ...
259
+
260
+ @overload
261
+ def get_export_from_serialized_record(
262
+ self,
263
+ record_dict: dict,
264
+ representation: Literal[ExportRepresentation.XML],
265
+ export_code: str | None = None,
266
+ export_mimetype: str | None = None,
267
+ ) -> Element: ...
268
+
269
+ def get_export_from_serialized_record(
270
+ self,
271
+ record_dict: dict,
272
+ representation: Literal[
273
+ ExportRepresentation.RESPONSE, ExportRepresentation.DICTIONARY, ExportRepresentation.XML
274
+ ],
275
+ export_code: str | None = None,
276
+ export_mimetype: str | None = None,
277
+ ):
278
+ """Retrieve and prepare the export of a record item based on the specified parameters.
279
+
280
+ This function processes the input `record_item` and converts it to the appropriate
281
+ export format based on the `representation` and either `export_code` or `export_mimetype`.
282
+ It ensures strict constraints on input validity and raises errors when conditions
283
+ are violated.
284
+
285
+ Args:
286
+ record_dict: The record item serialized as a dictionary
287
+ representation: The desired output representation of the export. It must be one
288
+ of the defined values of `ExportRepresentation`.
289
+ export_code: The code representing the type of export to retrieve. Only one of
290
+ `export_code` or `export_mimetype` should be provided.
291
+ export_mimetype: The MIME type representing the type of export to retrieve. Only
292
+ one of `export_code` or `export_mimetype` should be provided.
293
+
294
+ Raises:
295
+ ValueError: Raised if both `export_code` and `export_mimetype` are None.
296
+ ValueError: Raised if both `export_code` and `export_mimetype` are provided
297
+ simultaneously.
298
+ ValueError: Raised if no export is found for the given `export_code` or
299
+ `export_mimetype`.
300
+
301
+ Returns:
302
+ Union[Tuple[str, int, dict], dict, etree._Element]: Depending on the `representation`
303
+ specified:
304
+ - If `ExportRepresentation.RESPONSE`, returns a tuple with the serialized export,
305
+ HTTP status code (200), and the HTTP headers containing content type and
306
+ disposition.
307
+ - If `ExportRepresentation.DICTIONARY`, returns a dictionary representation of
308
+ the exported record item.
309
+ - If `ExportRepresentation.XML`, returns an XML element structure of the exported
310
+ record.
311
+
312
+ """
313
+ model = self.models_by_schema[record_dict["$schema"]]
314
+ if export_mimetype:
315
+ if export_code is not None:
316
+ raise ValueError("Only one of the parameters export_code/export_mimetype must be set, both are set")
317
+ model_export = model.get_export_by_mimetype(mimetype=export_mimetype)
318
+ elif export_code:
319
+ model_export = model.get_export_by_code(code=export_code)
320
+ else:
321
+ raise ValueError("One of the parameters export_code/export_mimetype must be set, both are None")
322
+
323
+ if model_export is None:
324
+ raise ValueError("No export found for the given mimetype or code")
325
+
326
+ exported_record = model_export.serializer.serialize_object(record_dict)
327
+
328
+ match representation:
329
+ case ExportRepresentation.RESPONSE:
330
+ filename = f"{record_dict['id']}{model_export.extension}"
331
+ headers = {
332
+ "Content-Type": model_export.mimetype,
333
+ "Content-Disposition": f"attachment; filename={filename}",
334
+ }
335
+ return (exported_record, 200, headers)
336
+
337
+ case ExportRepresentation.DICTIONARY:
338
+ if isinstance(exported_record, str):
339
+ exported_record = json.loads(exported_record)
340
+ return exported_record
341
+
342
+ case ExportRepresentation.XML:
343
+ return fromstring(exported_record)
@@ -35,6 +35,7 @@ from urllib.parse import urljoin
35
35
 
36
36
  from signposting import AbsoluteURI, LinkRel, Signpost
37
37
 
38
+ from oarepo_runtime.ext import ExportRepresentation
38
39
  from oarepo_runtime.proxies import current_runtime
39
40
 
40
41
 
@@ -277,9 +278,7 @@ def landing_page_signpost_links_list(datacite_dict: dict, record_dict: dict, sho
277
278
  model = current_runtime.models_by_schema[record_dict["$schema"]]
278
279
 
279
280
  # author - prvni tri
280
- data = datacite_dict["data"]
281
- attributes = data["attributes"]
282
- creators = attributes.get("creators", [])
281
+ creators = datacite_dict.get("creators", [])
283
282
  if short:
284
283
  creators = creators[:3]
285
284
  for attribute in creators:
@@ -289,7 +288,9 @@ def landing_page_signpost_links_list(datacite_dict: dict, record_dict: dict, sho
289
288
  )
290
289
 
291
290
  # cite-as = DOI
292
- signposting_links.append(Signpost(rel=LinkRel.cite_as, target=urljoin("https://doi.org/", attributes.get("doi"))))
291
+ signposting_links.append(
292
+ Signpost(rel=LinkRel.cite_as, target=urljoin("https://doi.org/", datacite_dict.get("doi")))
293
+ )
293
294
 
294
295
  # describedby
295
296
  for model_export in model.exports:
@@ -319,7 +320,7 @@ def landing_page_signpost_links_list(datacite_dict: dict, record_dict: dict, sho
319
320
  )
320
321
 
321
322
  # license
322
- for attribute in attributes.get("rightsList"):
323
+ for attribute in datacite_dict.get("rightsList", []):
323
324
  # check for schemeUri, rightsIdentifier and 'rightsIdentifierScheme' == SPDX, fallback rightsUri, else nothing
324
325
  license_url = attribute.get("rightsUri")
325
326
  if (
@@ -331,7 +332,7 @@ def landing_page_signpost_links_list(datacite_dict: dict, record_dict: dict, sho
331
332
  signposting_links.append(Signpost(rel=LinkRel.license, target=license_url))
332
333
 
333
334
  # type
334
- schema_org = attributes.get("types", {}).get("schemaOrg")
335
+ schema_org = datacite_dict.get("types", {}).get("schemaOrg")
335
336
  if schema_org:
336
337
  resource_type_url = "https://schema.org/" + schema_org
337
338
  signposting_links.append(Signpost(rel=LinkRel.type, target=resource_type_url))
@@ -342,19 +343,19 @@ def landing_page_signpost_links_list(datacite_dict: dict, record_dict: dict, sho
342
343
 
343
344
  def record_dict_to_linkset(record_dict: dict) -> str:
344
345
  """Create a linkset from the dictionary of a record item. Get datacite to build linkset from model exports."""
345
- model = current_runtime.models_by_schema[record_dict["$schema"]]
346
- datacite_export = model.get_export_by_mimetype("application/vnd.datacite.datacite+json")
347
- if not datacite_export:
348
- return ""
349
- datacite_dict = datacite_export.serializer.serialize_object(record_dict)
346
+ datacite_dict = current_runtime.get_export_from_serialized_record(
347
+ record_dict=record_dict,
348
+ representation=ExportRepresentation.DICTIONARY,
349
+ export_mimetype="application/vnd.datacite.datacite+json",
350
+ )
350
351
  return create_linkset(datacite_dict, record_dict)
351
352
 
352
353
 
353
354
  def record_dict_to_json_linkset(record_dict: dict) -> dict[str, list[dict[str, Any]]]:
354
355
  """Create a JSON linkset from the dictionary of a record item. Get datacite to build linkset from model exports."""
355
- model = current_runtime.models_by_schema[record_dict["$schema"]]
356
- datacite_export = model.get_export_by_mimetype("application/vnd.datacite.datacite+json")
357
- if not datacite_export:
358
- return {}
359
- datacite_dict = datacite_export.serializer.serialize_object(record_dict)
356
+ datacite_dict = current_runtime.get_export_from_serialized_record(
357
+ record_dict=record_dict,
358
+ representation=ExportRepresentation.DICTIONARY,
359
+ export_mimetype="application/vnd.datacite.datacite+json",
360
+ )
360
361
  return create_linkset_json(datacite_dict, record_dict)