oarepo-runtime 2.0.0.dev37__py3-none-any.whl → 2.0.0.dev39__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 +1 -1
 - oarepo_runtime/api.py +21 -0
 - oarepo_runtime/ext.py +11 -2
 - oarepo_runtime/info/views.py +2 -3
 - oarepo_runtime/records/systemfields/relations.py +307 -0
 - oarepo_runtime/resources/signposting/__init__.py +365 -0
 - {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev39.dist-info}/METADATA +2 -1
 - {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev39.dist-info}/RECORD +11 -9
 - {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev39.dist-info}/WHEEL +0 -0
 - {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev39.dist-info}/entry_points.txt +0 -0
 - {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev39.dist-info}/licenses/LICENSE +0 -0
 
    
        oarepo_runtime/__init__.py
    CHANGED
    
    
    
        oarepo_runtime/api.py
    CHANGED
    
    | 
         @@ -153,6 +153,7 @@ class Model[ 
     | 
|
| 
       153 
153 
     | 
    
         
             
                    model_metadata: ModelMetadata | None = None,
         
     | 
| 
       154 
154 
     | 
    
         
             
                    features: Mapping[str, Any] | None = None,
         
     | 
| 
       155 
155 
     | 
    
         
             
                    imports: list[Import] | None = None,
         
     | 
| 
      
 156 
     | 
    
         
            +
                    ui_blueprint_name: str | None = None,
         
     | 
| 
       156 
157 
     | 
    
         
             
                ):
         
     | 
| 
       157 
158 
     | 
    
         
             
                    """Initialize the model configuration.
         
     | 
| 
       158 
159 
     | 
    
         | 
| 
         @@ -179,6 +180,7 @@ class Model[ 
     | 
|
| 
       179 
180 
     | 
    
         
             
                    :param features: Features of the model. Filled by the feature presets themselves during registration.
         
     | 
| 
       180 
181 
     | 
    
         
             
                    :param imports: List of import formats that can be used to import the record.
         
     | 
| 
       181 
182 
     | 
    
         
             
                        If not provided, no imports are available.
         
     | 
| 
      
 183 
     | 
    
         
            +
                    :param ui_blueprint_name: Name of the UI blueprint
         
     | 
| 
       182 
184 
     | 
    
         
             
                    """
         
     | 
| 
       183 
185 
     | 
    
         
             
                    self._code = code
         
     | 
| 
       184 
186 
     | 
    
         
             
                    self._name = name
         
     | 
| 
         @@ -203,6 +205,7 @@ class Model[ 
     | 
|
| 
       203 
205 
     | 
    
         
             
                    self._imports = imports or []
         
     | 
| 
       204 
206 
     | 
    
         
             
                    self._model_metadata = model_metadata
         
     | 
| 
       205 
207 
     | 
    
         
             
                    self._features = features
         
     | 
| 
      
 208 
     | 
    
         
            +
                    self._ui_blueprint_name = ui_blueprint_name
         
     | 
| 
       206 
209 
     | 
    
         | 
| 
       207 
210 
     | 
    
         
             
                @property
         
     | 
| 
       208 
211 
     | 
    
         
             
                def code(self) -> str:
         
     | 
| 
         @@ -302,6 +305,11 @@ class Model[ 
     | 
|
| 
       302 
305 
     | 
    
         
             
                    """Get the API blueprint name for the model."""
         
     | 
| 
       303 
306 
     | 
    
         
             
                    return cast("str", self.resource_config.blueprint_name)
         
     | 
| 
       304 
307 
     | 
    
         | 
| 
      
 308 
     | 
    
         
            +
                @property
         
     | 
| 
      
 309 
     | 
    
         
            +
                def ui_blueprint_name(self) -> str | None:
         
     | 
| 
      
 310 
     | 
    
         
            +
                    """Get the API blueprint name for the model."""
         
     | 
| 
      
 311 
     | 
    
         
            +
                    return self._ui_blueprint_name
         
     | 
| 
      
 312 
     | 
    
         
            +
             
     | 
| 
       305 
313 
     | 
    
         
             
                @property
         
     | 
| 
       306 
314 
     | 
    
         
             
                def record_pid_type(self) -> str | None:
         
     | 
| 
       307 
315 
     | 
    
         
             
                    """Get the PID type for the model."""
         
     | 
| 
         @@ -342,6 +350,12 @@ class Model[ 
     | 
|
| 
       342 
350 
     | 
    
         
             
                    """Get the API URL for the model."""
         
     | 
| 
       343 
351 
     | 
    
         
             
                    return cast("str", invenio_url_for(f"{self.api_blueprint_name}.{view_name}", **kwargs))
         
     | 
| 
       344 
352 
     | 
    
         | 
| 
      
 353 
     | 
    
         
            +
                def ui_url(self, view_name: str, **kwargs: Any) -> str | None:
         
     | 
| 
      
 354 
     | 
    
         
            +
                    """Get the UI URL for the model."""
         
     | 
| 
      
 355 
     | 
    
         
            +
                    if self.ui_blueprint_name is None:
         
     | 
| 
      
 356 
     | 
    
         
            +
                        return None
         
     | 
| 
      
 357 
     | 
    
         
            +
                    return cast("str", invenio_url_for(f"{self.ui_blueprint_name}.{view_name}", **kwargs))
         
     | 
| 
      
 358 
     | 
    
         
            +
             
     | 
| 
       345 
359 
     | 
    
         
             
                @cached_property
         
     | 
| 
       346 
360 
     | 
    
         
             
                def resource_config(self) -> RC:
         
     | 
| 
       347 
361 
     | 
    
         
             
                    """Get the resource configuration."""
         
     | 
| 
         @@ -374,6 +388,13 @@ class Model[ 
     | 
|
| 
       374 
388 
     | 
    
         
             
                    """Get all exportable response handlers."""
         
     | 
| 
       375 
389 
     | 
    
         
             
                    return self._exports
         
     | 
| 
       376 
390 
     | 
    
         | 
| 
      
 391 
     | 
    
         
            +
                def get_export_by_mimetype(self, mimetype: str) -> Export | None:
         
     | 
| 
      
 392 
     | 
    
         
            +
                    """Get an export by mimetype."""
         
     | 
| 
      
 393 
     | 
    
         
            +
                    for export in self._exports:
         
     | 
| 
      
 394 
     | 
    
         
            +
                        if export.mimetype == mimetype:
         
     | 
| 
      
 395 
     | 
    
         
            +
                            return export
         
     | 
| 
      
 396 
     | 
    
         
            +
                    return None
         
     | 
| 
      
 397 
     | 
    
         
            +
             
     | 
| 
       377 
398 
     | 
    
         
             
                @property
         
     | 
| 
       378 
399 
     | 
    
         
             
                def response_handlers(self) -> dict[str, ResponseHandler]:
         
     | 
| 
       379 
400 
     | 
    
         
             
                    """Get all response handlers from the resource configuration."""
         
     | 
    
        oarepo_runtime/ext.py
    CHANGED
    
    | 
         @@ -177,6 +177,12 @@ class OARepoRuntime: 
     | 
|
| 
       177 
177 
     | 
    
         
             
                        raise ValueError("Need to pass a record instance, got None")
         
     | 
| 
       178 
178 
     | 
    
         
             
                    return self.get_record_service_for_record_class(type(record))
         
     | 
| 
       179 
179 
     | 
    
         | 
| 
      
 180 
     | 
    
         
            +
                def get_model_for_record(self, record: Any) -> Model:
         
     | 
| 
      
 181 
     | 
    
         
            +
                    """Retrieve the associated service for a given record."""
         
     | 
| 
      
 182 
     | 
    
         
            +
                    if record is None:
         
     | 
| 
      
 183 
     | 
    
         
            +
                        raise ValueError("Need to pass a record instance, got None")
         
     | 
| 
      
 184 
     | 
    
         
            +
                    return self.get_model_for_record_class(type(record))
         
     | 
| 
      
 185 
     | 
    
         
            +
             
     | 
| 
       180 
186 
     | 
    
         
             
                def get_file_service_for_record(self, record: Any) -> FileService | None:
         
     | 
| 
       181 
187 
     | 
    
         
             
                    """Return the file service for the given record (draft or published)."""
         
     | 
| 
       182 
188 
     | 
    
         
             
                    model = self.models_by_record_class.get(type(record))
         
     | 
| 
         @@ -188,13 +194,16 @@ class OARepoRuntime: 
     | 
|
| 
       188 
194 
     | 
    
         
             
                    return model.file_service
         
     | 
| 
       189 
195 
     | 
    
         | 
| 
       190 
196 
     | 
    
         
             
                def get_record_service_for_record_class(self, record_cls: type[RecordBase]) -> RecordService:
         
     | 
| 
      
 197 
     | 
    
         
            +
                    """Retrieve the service associated with a given record class."""
         
     | 
| 
      
 198 
     | 
    
         
            +
                    return self.get_model_for_record_class(record_cls).service
         
     | 
| 
      
 199 
     | 
    
         
            +
             
     | 
| 
      
 200 
     | 
    
         
            +
                def get_model_for_record_class(self, record_cls: type[RecordBase]) -> Model:
         
     | 
| 
       191 
201 
     | 
    
         
             
                    """Retrieve the service associated with a given record class."""
         
     | 
| 
       192 
202 
     | 
    
         
             
                    for t in record_cls.mro():
         
     | 
| 
       193 
203 
     | 
    
         
             
                        if t is RecordBase:
         
     | 
| 
       194 
204 
     | 
    
         
             
                            break
         
     | 
| 
       195 
205 
     | 
    
         
             
                        if t in self.models_by_record_class:
         
     | 
| 
       196 
     | 
    
         
            -
                             
     | 
| 
       197 
     | 
    
         
            -
                            return model.service
         
     | 
| 
      
 206 
     | 
    
         
            +
                            return self.models_by_record_class[t]
         
     | 
| 
       198 
207 
     | 
    
         
             
                    raise KeyError(f"No service found for record class '{record_cls.__name__}'.")
         
     | 
| 
       199 
208 
     | 
    
         | 
| 
       200 
209 
     | 
    
         
             
                @cached_property
         
     | 
    
        oarepo_runtime/info/views.py
    CHANGED
    
    | 
         @@ -1,13 +1,12 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            #
         
     | 
| 
       2 
2 
     | 
    
         
             
            # Copyright (c) 2025 CESNET z.s.p.o.
         
     | 
| 
       3 
3 
     | 
    
         
             
            #
         
     | 
| 
       4 
     | 
    
         
            -
            # This file is a part of oarepo-runtime (see  
     | 
| 
      
 4 
     | 
    
         
            +
            # This file is a part of oarepo-runtime (see https://github.com/oarepo/oarepo-runtime).
         
     | 
| 
       5 
5 
     | 
    
         
             
            #
         
     | 
| 
       6 
6 
     | 
    
         
             
            # oarepo-runtime is free software; you can redistribute it and/or modify it
         
     | 
| 
       7 
7 
     | 
    
         
             
            # under the terms of the MIT License; see LICENSE file for more details.
         
     | 
| 
       8 
8 
     | 
    
         
             
            #
         
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
            """Info Resource."""
         
     | 
| 
      
 9 
     | 
    
         
            +
            """Resource for serving machine-readable information about the repository."""
         
     | 
| 
       11 
10 
     | 
    
         | 
| 
       12 
11 
     | 
    
         
             
            from __future__ import annotations
         
     | 
| 
       13 
12 
     | 
    
         | 
| 
         @@ -0,0 +1,307 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            #
         
     | 
| 
      
 2 
     | 
    
         
            +
            # Copyright (c) 2025 CESNET z.s.p.o.
         
     | 
| 
      
 3 
     | 
    
         
            +
            #
         
     | 
| 
      
 4 
     | 
    
         
            +
            # This file is a part of oarepo-runtime (see https://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 
     | 
    
         
            +
            """A relation field that allows arbitrarily nested lists of relations."""
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
            from __future__ import annotations
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            from itertools import zip_longest
         
     | 
| 
      
 14 
     | 
    
         
            +
            from typing import TYPE_CHECKING, Any, override
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
            from invenio_records.dictutils import dict_lookup, dict_set
         
     | 
| 
      
 17 
     | 
    
         
            +
            from invenio_records.systemfields.relations import (
         
     | 
| 
      
 18 
     | 
    
         
            +
                InvalidRelationValue,
         
     | 
| 
      
 19 
     | 
    
         
            +
                ListRelation,
         
     | 
| 
      
 20 
     | 
    
         
            +
                RelationListResult,
         
     | 
| 
      
 21 
     | 
    
         
            +
            )
         
     | 
| 
      
 22 
     | 
    
         
            +
            from invenio_records_resources.records.systemfields.relations import (
         
     | 
| 
      
 23 
     | 
    
         
            +
                PIDRelation,
         
     | 
| 
      
 24 
     | 
    
         
            +
            )
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            if TYPE_CHECKING:
         
     | 
| 
      
 27 
     | 
    
         
            +
                from collections.abc import Callable, Generator
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                from invenio_records.api import Record
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
            class ArbitraryNestedListResult(RelationListResult):
         
     | 
| 
      
 33 
     | 
    
         
            +
                """Relation access result."""
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                @override
         
     | 
| 
      
 36 
     | 
    
         
            +
                def __call__(self, force: bool = True):
         
     | 
| 
      
 37 
     | 
    
         
            +
                    """Resolve the relation."""
         
     | 
| 
      
 38 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 39 
     | 
    
         
            +
                        # not as efficient as it could be as we create the list of lists first
         
     | 
| 
      
 40 
     | 
    
         
            +
                        # before returning the iterator, but simpler to implement
         
     | 
| 
      
 41 
     | 
    
         
            +
                        return iter(
         
     | 
| 
      
 42 
     | 
    
         
            +
                            _for_each_deep(
         
     | 
| 
      
 43 
     | 
    
         
            +
                                self._lookup_data(),
         
     | 
| 
      
 44 
     | 
    
         
            +
                                lambda v: self.resolve(v[self._value_key_suffix]),
         
     | 
| 
      
 45 
     | 
    
         
            +
                                levels=len(self.field.path_elements),
         
     | 
| 
      
 46 
     | 
    
         
            +
                            )
         
     | 
| 
      
 47 
     | 
    
         
            +
                        )
         
     | 
| 
      
 48 
     | 
    
         
            +
                    except KeyError:
         
     | 
| 
      
 49 
     | 
    
         
            +
                        return None
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                def _lookup_data(self) -> Any:
         
     | 
| 
      
 52 
     | 
    
         
            +
                    """Lookup the data from the record."""
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                    # recursively lookup the data following the path elements. The end of the path
         
     | 
| 
      
 55 
     | 
    
         
            +
                    # must always be an array of objects.
         
     | 
| 
      
 56 
     | 
    
         
            +
                    def _lookup(r: Any, paths: list[str]) -> Any:
         
     | 
| 
      
 57 
     | 
    
         
            +
                        if not paths:
         
     | 
| 
      
 58 
     | 
    
         
            +
                            if self.field.relation_field:
         
     | 
| 
      
 59 
     | 
    
         
            +
                                try:
         
     | 
| 
      
 60 
     | 
    
         
            +
                                    return dict_lookup(r, self.field.relation_field)
         
     | 
| 
      
 61 
     | 
    
         
            +
                                except KeyError:  # pragma: no cover
         
     | 
| 
      
 62 
     | 
    
         
            +
                                    return None
         
     | 
| 
      
 63 
     | 
    
         
            +
                            return r
         
     | 
| 
      
 64 
     | 
    
         
            +
                        try:
         
     | 
| 
      
 65 
     | 
    
         
            +
                            level_values = dict_lookup(r, paths[0])
         
     | 
| 
      
 66 
     | 
    
         
            +
                            if not isinstance(level_values, list):
         
     | 
| 
      
 67 
     | 
    
         
            +
                                raise InvalidRelationValue(  # pragma: no cover
         
     | 
| 
      
 68 
     | 
    
         
            +
                                    f'Invalid structure, expecting list at "{paths[0]}", got {level_values}. '
         
     | 
| 
      
 69 
     | 
    
         
            +
                                    f'Complete paths: "{self.field.path_elements}"'
         
     | 
| 
      
 70 
     | 
    
         
            +
                                )
         
     | 
| 
      
 71 
     | 
    
         
            +
                            ret = [_lookup(v, paths[1:]) for v in level_values]
         
     | 
| 
      
 72 
     | 
    
         
            +
                            return [v for v in ret if v is not None]
         
     | 
| 
      
 73 
     | 
    
         
            +
                        except KeyError:
         
     | 
| 
      
 74 
     | 
    
         
            +
                            return []
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                    return _lookup(self.record, self.field.path_elements)
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                @override
         
     | 
| 
      
 79 
     | 
    
         
            +
                def validate(self) -> None:
         
     | 
| 
      
 80 
     | 
    
         
            +
                    """Validate the field."""
         
     | 
| 
      
 81 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 82 
     | 
    
         
            +
                        values = self._lookup_data()
         
     | 
| 
      
 83 
     | 
    
         
            +
                        # not as efficient as it could be as we create the list of lists first
         
     | 
| 
      
 84 
     | 
    
         
            +
                        # before returning, but simpler to implement
         
     | 
| 
      
 85 
     | 
    
         
            +
                        _for_each_deep(
         
     | 
| 
      
 86 
     | 
    
         
            +
                            values,
         
     | 
| 
      
 87 
     | 
    
         
            +
                            lambda v: self._validate_single_value(v),
         
     | 
| 
      
 88 
     | 
    
         
            +
                            levels=len(self.field.path_elements),
         
     | 
| 
      
 89 
     | 
    
         
            +
                        )
         
     | 
| 
      
 90 
     | 
    
         
            +
                    except KeyError:  # pragma: no cover
         
     | 
| 
      
 91 
     | 
    
         
            +
                        return
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                def _validate_single_value(self, v: Any) -> None:
         
     | 
| 
      
 94 
     | 
    
         
            +
                    """Validate a single value."""
         
     | 
| 
      
 95 
     | 
    
         
            +
                    if isinstance(v, list):
         
     | 
| 
      
 96 
     | 
    
         
            +
                        raise InvalidRelationValue(f"Invalid value {v}, should not be list.")
         
     | 
| 
      
 97 
     | 
    
         
            +
                    relation_id = self._lookup_id(v)
         
     | 
| 
      
 98 
     | 
    
         
            +
                    if not self.exists(relation_id):
         
     | 
| 
      
 99 
     | 
    
         
            +
                        raise InvalidRelationValue(f"Invalid value {relation_id}.")
         
     | 
| 
      
 100 
     | 
    
         
            +
                    if self.value_check:  # pragma: no cover # not testing, copied from invenio
         
     | 
| 
      
 101 
     | 
    
         
            +
                        obj = self.resolve(v[self.field._value_key_suffix])  # noqa: SLF001 # private attr
         
     | 
| 
      
 102 
     | 
    
         
            +
                        self._value_check(self.value_check, obj)
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                @override
         
     | 
| 
      
 105 
     | 
    
         
            +
                def _apply_items(  # type: ignore[override]
         
     | 
| 
      
 106 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 107 
     | 
    
         
            +
                    func: Callable,
         
     | 
| 
      
 108 
     | 
    
         
            +
                    keys: list[str] | None = None,
         
     | 
| 
      
 109 
     | 
    
         
            +
                    attrs: list[str] | None = None,
         
     | 
| 
      
 110 
     | 
    
         
            +
                ) -> list[Any] | None:
         
     | 
| 
      
 111 
     | 
    
         
            +
                    """Iterate over the list of objects."""
         
     | 
| 
      
 112 
     | 
    
         
            +
                    # The attributes we want to get from the related record.
         
     | 
| 
      
 113 
     | 
    
         
            +
                    attrs = attrs or self.attrs
         
     | 
| 
      
 114 
     | 
    
         
            +
                    keys = keys or self.keys
         
     | 
| 
      
 115 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 116 
     | 
    
         
            +
                        # Get the list of objects we have to dereference/clean.
         
     | 
| 
      
 117 
     | 
    
         
            +
                        values = self._lookup_data()
         
     | 
| 
      
 118 
     | 
    
         
            +
                        return _for_each_deep(
         
     | 
| 
      
 119 
     | 
    
         
            +
                            values,
         
     | 
| 
      
 120 
     | 
    
         
            +
                            lambda v: func(v, keys, attrs),
         
     | 
| 
      
 121 
     | 
    
         
            +
                            levels=len(self.field.path_elements),
         
     | 
| 
      
 122 
     | 
    
         
            +
                        )
         
     | 
| 
      
 123 
     | 
    
         
            +
                    except KeyError:  # pragma: no cover
         
     | 
| 
      
 124 
     | 
    
         
            +
                        return None
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
            class ArbitraryNestedListRelation(ListRelation):
         
     | 
| 
      
 128 
     | 
    
         
            +
                """Arbitrary nested relation list type.
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                self.path_elements contain the segments of the path that are within lists.
         
     | 
| 
      
 131 
     | 
    
         
            +
                For example:
         
     | 
| 
      
 132 
     | 
    
         
            +
                - For paths like "a.b.c", path = [], relation_field="a.b.c"
         
     | 
| 
      
 133 
     | 
    
         
            +
                - For paths like "a.b.0.c", path = ["a.b"], relation_field="c"
         
     | 
| 
      
 134 
     | 
    
         
            +
                - For paths like "a.0.b.1.c", path = ["a", "b"], relation_field="c"
         
     | 
| 
      
 135 
     | 
    
         
            +
                - For paths like "a.1", path = ["a"], relation_field=None
         
     | 
| 
      
 136 
     | 
    
         
            +
             
     | 
| 
      
 137 
     | 
    
         
            +
                ids and values are stored as lists that can contain other lists arbitrarily nested.
         
     | 
| 
      
 138 
     | 
    
         
            +
                The total depth of nesting is given by the length of self.path_elements + 1 if self.relation_field is not None
         
     | 
| 
      
 139 
     | 
    
         
            +
                or length of self.path_elements if self.relation_field is None.
         
     | 
| 
      
 140 
     | 
    
         
            +
                """
         
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
                result_cls = ArbitraryNestedListResult
         
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
                def __init__(
         
     | 
| 
      
 145 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 146 
     | 
    
         
            +
                    *args: Any,
         
     | 
| 
      
 147 
     | 
    
         
            +
                    array_paths: list[str] | None = None,
         
     | 
| 
      
 148 
     | 
    
         
            +
                    relation_field: str | None = None,
         
     | 
| 
      
 149 
     | 
    
         
            +
                    **kwargs: Any,
         
     | 
| 
      
 150 
     | 
    
         
            +
                ):
         
     | 
| 
      
 151 
     | 
    
         
            +
                    """Initialize the relation."""
         
     | 
| 
      
 152 
     | 
    
         
            +
                    if not array_paths:
         
     | 
| 
      
 153 
     | 
    
         
            +
                        raise ValueError("array_paths are required for ArbitraryNestedListRelation.")
         
     | 
| 
      
 154 
     | 
    
         
            +
                    self.path_elements = array_paths
         
     | 
| 
      
 155 
     | 
    
         
            +
                    super().__init__(*args, relation_field=relation_field, **kwargs)
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
      
 157 
     | 
    
         
            +
                @override
         
     | 
| 
      
 158 
     | 
    
         
            +
                def exists_many(self, ids: Any) -> bool:  # type: ignore[override]
         
     | 
| 
      
 159 
     | 
    
         
            +
                    """Return True if all ids exists."""
         
     | 
| 
      
 160 
     | 
    
         
            +
             
     | 
| 
      
 161 
     | 
    
         
            +
                    # ids is a list that might recursively contain lists that contain ids
         
     | 
| 
      
 162 
     | 
    
         
            +
                    def flatten(nested_list: Any) -> Generator[Any]:
         
     | 
| 
      
 163 
     | 
    
         
            +
                        for item in nested_list:
         
     | 
| 
      
 164 
     | 
    
         
            +
                            if isinstance(item, (list, tuple)):
         
     | 
| 
      
 165 
     | 
    
         
            +
                                yield from flatten(item)
         
     | 
| 
      
 166 
     | 
    
         
            +
                            else:
         
     | 
| 
      
 167 
     | 
    
         
            +
                                yield item
         
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
      
 169 
     | 
    
         
            +
                    return all(self.exists(i) for i in flatten(ids))
         
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
      
 171 
     | 
    
         
            +
                @override
         
     | 
| 
      
 172 
     | 
    
         
            +
                def parse_value(self, value: list[Any] | tuple[Any]) -> list[Any]:  # type: ignore[override]
         
     | 
| 
      
 173 
     | 
    
         
            +
                    """Parse a record (or ID) to the ID to be stored."""
         
     | 
| 
      
 174 
     | 
    
         
            +
                    return _for_each_deep(value, lambda v: self._parse_single_value(v), levels=len(self.path_elements))
         
     | 
| 
      
 175 
     | 
    
         
            +
             
     | 
| 
      
 176 
     | 
    
         
            +
                def _parse_single_value(self, value: Any) -> Any:
         
     | 
| 
      
 177 
     | 
    
         
            +
                    """Parse a single value using the parent class method.
         
     | 
| 
      
 178 
     | 
    
         
            +
             
     | 
| 
      
 179 
     | 
    
         
            +
                    Note: we are skipping the list's parse_value here and calling
         
     | 
| 
      
 180 
     | 
    
         
            +
                    the next one after ListRelation in mro chain. That might be, for example,
         
     | 
| 
      
 181 
     | 
    
         
            +
                    PIDRelation.parse_value
         
     | 
| 
      
 182 
     | 
    
         
            +
                    """
         
     | 
| 
      
 183 
     | 
    
         
            +
                    if self.relation_field:
         
     | 
| 
      
 184 
     | 
    
         
            +
                        try:
         
     | 
| 
      
 185 
     | 
    
         
            +
                            return super(ListRelation, self).parse_value(dict_lookup(value, self.relation_field))
         
     | 
| 
      
 186 
     | 
    
         
            +
                        except KeyError:  # pragma: no cover
         
     | 
| 
      
 187 
     | 
    
         
            +
                            return None
         
     | 
| 
      
 188 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 189 
     | 
    
         
            +
                        return super(ListRelation, self).parse_value(value)
         
     | 
| 
      
 190 
     | 
    
         
            +
             
     | 
| 
      
 191 
     | 
    
         
            +
                @override
         
     | 
| 
      
 192 
     | 
    
         
            +
                def set_value(
         
     | 
| 
      
 193 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 194 
     | 
    
         
            +
                    record: Record,
         
     | 
| 
      
 195 
     | 
    
         
            +
                    value: list[Any] | tuple[Any],
         
     | 
| 
      
 196 
     | 
    
         
            +
                ) -> None:  # type: ignore[override]
         
     | 
| 
      
 197 
     | 
    
         
            +
                    """Set the relation value."""
         
     | 
| 
      
 198 
     | 
    
         
            +
                    store_values = self.parse_value(value)
         
     | 
| 
      
 199 
     | 
    
         
            +
             
     | 
| 
      
 200 
     | 
    
         
            +
                    if not self.exists_many(store_values):
         
     | 
| 
      
 201 
     | 
    
         
            +
                        raise InvalidRelationValue(f'One of the values "{store_values}" is invalid.')
         
     | 
| 
      
 202 
     | 
    
         
            +
             
     | 
| 
      
 203 
     | 
    
         
            +
                    total_depth = len(self.path_elements)
         
     | 
| 
      
 204 
     | 
    
         
            +
             
     | 
| 
      
 205 
     | 
    
         
            +
                    for path_indices, path_value in _deep_enumerate(store_values, total_depth):
         
     | 
| 
      
 206 
     | 
    
         
            +
                        r: Any = record
         
     | 
| 
      
 207 
     | 
    
         
            +
                        self._set_value_at_path(r, path_indices, {self._value_key_suffix: path_value})
         
     | 
| 
      
 208 
     | 
    
         
            +
             
     | 
| 
      
 209 
     | 
    
         
            +
                def _set_value_at_path(self, r: Any, path_indices: list[int], path_value: Any) -> None:
         
     | 
| 
      
 210 
     | 
    
         
            +
                    """Set the value at the given path indices."""
         
     | 
| 
      
 211 
     | 
    
         
            +
                    pe = [
         
     | 
| 
      
 212 
     | 
    
         
            +
                        *self.path_elements,
         
     | 
| 
      
 213 
     | 
    
         
            +
                    ]
         
     | 
| 
      
 214 
     | 
    
         
            +
                    if self.relation_field:
         
     | 
| 
      
 215 
     | 
    
         
            +
                        pe.append(self.relation_field)
         
     | 
| 
      
 216 
     | 
    
         
            +
             
     | 
| 
      
 217 
     | 
    
         
            +
                    # pe might be 1 longer than path_indices, that's why we use zip_longest
         
     | 
| 
      
 218 
     | 
    
         
            +
                    zipped = list(zip_longest(pe, path_indices, fillvalue=None))
         
     | 
| 
      
 219 
     | 
    
         
            +
                    for idx, (subpath, index) in enumerate(zipped[:-1]):
         
     | 
| 
      
 220 
     | 
    
         
            +
                        if not subpath:
         
     | 
| 
      
 221 
     | 
    
         
            +
                            raise InvalidRelationValue(  # pragma: no cover
         
     | 
| 
      
 222 
     | 
    
         
            +
                                f"Invalid structure, missing key at index {idx} in [{self.path_elements}, {path_indices}]."
         
     | 
| 
      
 223 
     | 
    
         
            +
                            )
         
     | 
| 
      
 224 
     | 
    
         
            +
                        if index is None:
         
     | 
| 
      
 225 
     | 
    
         
            +
                            raise InvalidRelationValue(  # pragma: no cover
         
     | 
| 
      
 226 
     | 
    
         
            +
                                f"Invalid structure, missing index at {subpath} in [{self.path_elements}, {path_indices}]."
         
     | 
| 
      
 227 
     | 
    
         
            +
                            )
         
     | 
| 
      
 228 
     | 
    
         
            +
                        r = self._set_default_value_at_path(r, subpath, index, path_indices)
         
     | 
| 
      
 229 
     | 
    
         
            +
             
     | 
| 
      
 230 
     | 
    
         
            +
                    last_path, last_index = zipped[-1]
         
     | 
| 
      
 231 
     | 
    
         
            +
                    if last_path is None:  # pragma: no cover
         
     | 
| 
      
 232 
     | 
    
         
            +
                        raise InvalidRelationValue("Implementation error.")
         
     | 
| 
      
 233 
     | 
    
         
            +
                    if last_index is None:
         
     | 
| 
      
 234 
     | 
    
         
            +
                        # we have a relation_field at the end, so set it directly
         
     | 
| 
      
 235 
     | 
    
         
            +
                        dict_set(r, last_path, path_value)
         
     | 
| 
      
 236 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 237 
     | 
    
         
            +
                        # no relation_field at the end, so we set the whole object at the index
         
     | 
| 
      
 238 
     | 
    
         
            +
                        try:
         
     | 
| 
      
 239 
     | 
    
         
            +
                            val = dict_lookup(r, last_path)
         
     | 
| 
      
 240 
     | 
    
         
            +
                        except KeyError:
         
     | 
| 
      
 241 
     | 
    
         
            +
                            val = []
         
     | 
| 
      
 242 
     | 
    
         
            +
                            dict_set(r, last_path, val)
         
     | 
| 
      
 243 
     | 
    
         
            +
                        if last_index < len(val):
         
     | 
| 
      
 244 
     | 
    
         
            +
                            val[last_index] = path_value
         
     | 
| 
      
 245 
     | 
    
         
            +
                        elif last_index == len(val):
         
     | 
| 
      
 246 
     | 
    
         
            +
                            val.append(path_value)
         
     | 
| 
      
 247 
     | 
    
         
            +
                        else:
         
     | 
| 
      
 248 
     | 
    
         
            +
                            raise InvalidRelationValue(  # pragma: no cover # just sanity check
         
     | 
| 
      
 249 
     | 
    
         
            +
                                f"Invalid structure, missing index {last_index} "
         
     | 
| 
      
 250 
     | 
    
         
            +
                                f"at {last_path} in [{self.path_elements}, {path_indices}]."
         
     | 
| 
      
 251 
     | 
    
         
            +
                            )
         
     | 
| 
      
 252 
     | 
    
         
            +
             
     | 
| 
      
 253 
     | 
    
         
            +
                def _set_default_value_at_path(self, r: Any, subpath: str, index: int, path_indices: list[int]) -> Any:
         
     | 
| 
      
 254 
     | 
    
         
            +
                    """Set default value of [] at the given path if missing."""
         
     | 
| 
      
 255 
     | 
    
         
            +
                    # look up the subpath and create it if missing
         
     | 
| 
      
 256 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 257 
     | 
    
         
            +
                        val = dict_lookup(r, subpath)
         
     | 
| 
      
 258 
     | 
    
         
            +
                    except KeyError:
         
     | 
| 
      
 259 
     | 
    
         
            +
                        dict_set(r, subpath, [])
         
     | 
| 
      
 260 
     | 
    
         
            +
                        val = dict_lookup(r, subpath)
         
     | 
| 
      
 261 
     | 
    
         
            +
                    if not isinstance(val, list):
         
     | 
| 
      
 262 
     | 
    
         
            +
                        raise InvalidRelationValue(  # pragma: no cover
         
     | 
| 
      
 263 
     | 
    
         
            +
                            f"Invalid structure, expecting list at {subpath} in [{self.path_elements}, {path_indices}]."
         
     | 
| 
      
 264 
     | 
    
         
            +
                        )
         
     | 
| 
      
 265 
     | 
    
         
            +
             
     | 
| 
      
 266 
     | 
    
         
            +
                    # now we have the array - if the index is within array, return the value at the index
         
     | 
| 
      
 267 
     | 
    
         
            +
                    if index < len(val):
         
     | 
| 
      
 268 
     | 
    
         
            +
                        return val[index]
         
     | 
| 
      
 269 
     | 
    
         
            +
             
     | 
| 
      
 270 
     | 
    
         
            +
                    # if the index is exactly at the end of the array, we can append a new default value,
         
     | 
| 
      
 271 
     | 
    
         
            +
                    # which is always an empty dict
         
     | 
| 
      
 272 
     | 
    
         
            +
                    if index == len(val):
         
     | 
| 
      
 273 
     | 
    
         
            +
                        # append new default value which is always empty dict
         
     | 
| 
      
 274 
     | 
    
         
            +
                        r = {}
         
     | 
| 
      
 275 
     | 
    
         
            +
                        val.append(r)
         
     | 
| 
      
 276 
     | 
    
         
            +
                        return r
         
     | 
| 
      
 277 
     | 
    
         
            +
             
     | 
| 
      
 278 
     | 
    
         
            +
                    # we can not skip indices, so if that would happen, raise error
         
     | 
| 
      
 279 
     | 
    
         
            +
                    raise InvalidRelationValue(  # pragma: no cover
         
     | 
| 
      
 280 
     | 
    
         
            +
                        f"Invalid structure, missing index {index} at {subpath} in [{self.path_elements}, {path_indices}]."
         
     | 
| 
      
 281 
     | 
    
         
            +
                    )
         
     | 
| 
      
 282 
     | 
    
         
            +
             
     | 
| 
      
 283 
     | 
    
         
            +
             
     | 
| 
      
 284 
     | 
    
         
            +
            def _deep_enumerate(nested_list: Any, max_depth: int, depth: int = 0) -> Generator[tuple[list[int], Any]]:
         
     | 
| 
      
 285 
     | 
    
         
            +
                """Enumerate all non-list items in a nested list structure."""
         
     | 
| 
      
 286 
     | 
    
         
            +
                for index, item in enumerate(nested_list):
         
     | 
| 
      
 287 
     | 
    
         
            +
                    current_path = [index]
         
     | 
| 
      
 288 
     | 
    
         
            +
                    if depth < max_depth - 1 and isinstance(item, (list, tuple)):
         
     | 
| 
      
 289 
     | 
    
         
            +
                        for sub_path, sub_item in _deep_enumerate(item, max_depth, depth + 1):
         
     | 
| 
      
 290 
     | 
    
         
            +
                            yield current_path + sub_path, sub_item
         
     | 
| 
      
 291 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 292 
     | 
    
         
            +
                        yield current_path, item
         
     | 
| 
      
 293 
     | 
    
         
            +
             
     | 
| 
      
 294 
     | 
    
         
            +
             
     | 
| 
      
 295 
     | 
    
         
            +
            def _for_each_deep(nested_list: Any, func: Any, levels: int) -> list[Any]:
         
     | 
| 
      
 296 
     | 
    
         
            +
                """Apply a function to each non-list item in a nested list structure."""
         
     | 
| 
      
 297 
     | 
    
         
            +
                result = []
         
     | 
| 
      
 298 
     | 
    
         
            +
                for item in nested_list:
         
     | 
| 
      
 299 
     | 
    
         
            +
                    if isinstance(item, (list, tuple)) and levels > 1:
         
     | 
| 
      
 300 
     | 
    
         
            +
                        result.append(_for_each_deep(item, func, levels=levels - 1))
         
     | 
| 
      
 301 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 302 
     | 
    
         
            +
                        result.append(func(item))
         
     | 
| 
      
 303 
     | 
    
         
            +
                return result
         
     | 
| 
      
 304 
     | 
    
         
            +
             
     | 
| 
      
 305 
     | 
    
         
            +
             
     | 
| 
      
 306 
     | 
    
         
            +
            class PIDArbitraryNestedListRelation(ArbitraryNestedListRelation, PIDRelation):  # type: ignore[override, misc]
         
     | 
| 
      
 307 
     | 
    
         
            +
                """PID list relation type."""
         
     | 
| 
         @@ -0,0 +1,365 @@ 
     | 
|
| 
      
 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 
     | 
    
         
            +
            """Signposting functionality.
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
            Functions to create a list of signpost links record's landing page, export formats and file contents.
         
     | 
| 
      
 12 
     | 
    
         
            +
            Separate functions to create a complete linkset for the record item in application/linkset or application/linkset+json format.
         
     | 
| 
      
 13 
     | 
    
         
            +
            Function to format the linkset into a HTTP Link header.
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            Information about relation types can be found at: https://signposting.org/FAIR/#reltypes
         
     | 
| 
      
 16 
     | 
    
         
            +
            Excerpt with explanations about relation types:
         
     | 
| 
      
 17 
     | 
    
         
            +
            author 	        = The target of the link is a URI for an author of the resource that is the origin of the link.
         
     | 
| 
      
 18 
     | 
    
         
            +
            cite-as 	    = The target of the link is a persistent URI for the resource that is the origin of the link. When accessing the persistent URI, it redirects to that origin resource.
         
     | 
| 
      
 19 
     | 
    
         
            +
            describedby 	= The target of the link provides metadata that describes the resource that is the origin of the link. It is the inverse of the describes relation type.
         
     | 
| 
      
 20 
     | 
    
         
            +
            describes 	    = The origin of the link is a resource that provides metadata that describes the resource that is the target of the link. It is the inverse of the describedby relation type.
         
     | 
| 
      
 21 
     | 
    
         
            +
            type 	        = The target of the link is the URI for a class of resources to which the resource that is the origin of the link belongs.
         
     | 
| 
      
 22 
     | 
    
         
            +
            license 	    = The target of the link is the URI of a license that applies to the resource that is the origin of the link.
         
     | 
| 
      
 23 
     | 
    
         
            +
            item 	        = The origin of the link is a collection of resources and the target of the link is a resource that belongs to that collection. It is the inverse of the collection relation type.
         
     | 
| 
      
 24 
     | 
    
         
            +
            collection 	    = The origin of the link is a resource that belongs to a collection and the target of the link is the collection to which it belongs. It is the inverse of the item relation type.
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            item <-> collection
         
     | 
| 
      
 27 
     | 
    
         
            +
            describedby <-> describes
         
     | 
| 
      
 28 
     | 
    
         
            +
            """  # noqa: E501
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
            from __future__ import annotations
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
            from collections import defaultdict
         
     | 
| 
      
 33 
     | 
    
         
            +
            from typing import TYPE_CHECKING, Any, Literal, cast, overload
         
     | 
| 
      
 34 
     | 
    
         
            +
            from urllib.parse import urljoin
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
            if TYPE_CHECKING:
         
     | 
| 
      
 37 
     | 
    
         
            +
                from invenio_records_resources.services.records.results import RecordItem
         
     | 
| 
      
 38 
     | 
    
         
            +
            from signposting import AbsoluteURI, LinkRel, Signpost
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
            from oarepo_runtime.proxies import current_runtime
         
     | 
| 
      
 41 
     | 
    
         
            +
            from oarepo_runtime.typing import record_from_result
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
            def signpost_link_to_str(signpost_link: Signpost) -> str:
         
     | 
| 
      
 45 
     | 
    
         
            +
                """Convert a signpost link to string."""
         
     | 
| 
      
 46 
     | 
    
         
            +
                link_str = str(signpost_link)
         
     | 
| 
      
 47 
     | 
    
         
            +
                if link_str[:6] == "Link: ":
         
     | 
| 
      
 48 
     | 
    
         
            +
                    return f"{link_str[6:]}"
         
     | 
| 
      
 49 
     | 
    
         
            +
                raise ValueError(f"Invalid signpost link: {link_str}")  # pragma: no cover
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
            def signpost_link_to_dict(link: Signpost) -> dict[str, Any]:
         
     | 
| 
      
 53 
     | 
    
         
            +
                """Convert signpost link to a dictionary."""
         
     | 
| 
      
 54 
     | 
    
         
            +
                link_dict: dict[str, Any] = {"href": link.target}
         
     | 
| 
      
 55 
     | 
    
         
            +
                if link.type:
         
     | 
| 
      
 56 
     | 
    
         
            +
                    link_dict["type"] = link.type
         
     | 
| 
      
 57 
     | 
    
         
            +
                return link_dict
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
            def signpost_link_to_additional_link(
         
     | 
| 
      
 61 
     | 
    
         
            +
                link: Signpost, landing_page_url: str, as_dict: bool = True
         
     | 
| 
      
 62 
     | 
    
         
            +
            ) -> Signpost | dict[str, Any] | None:
         
     | 
| 
      
 63 
     | 
    
         
            +
                """Transform signpost link to additional link with inversed relation type..
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 66 
     | 
    
         
            +
                    link: A signpost link to transform
         
     | 
| 
      
 67 
     | 
    
         
            +
                    landing_page_url: landing page url which will be passed to href
         
     | 
| 
      
 68 
     | 
    
         
            +
                    as_dict: if true, return dict, else return Signpost
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                Returns: Additional link as dict or Signpost or None if relation type does no thave additional link.
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                """
         
     | 
| 
      
 73 
     | 
    
         
            +
                match link.rel:
         
     | 
| 
      
 74 
     | 
    
         
            +
                    case LinkRel.item:
         
     | 
| 
      
 75 
     | 
    
         
            +
                        if as_dict:
         
     | 
| 
      
 76 
     | 
    
         
            +
                            return {
         
     | 
| 
      
 77 
     | 
    
         
            +
                                "anchor": link.target,
         
     | 
| 
      
 78 
     | 
    
         
            +
                                str(LinkRel.collection): [{"href": landing_page_url, "type": "text/html"}],
         
     | 
| 
      
 79 
     | 
    
         
            +
                            }
         
     | 
| 
      
 80 
     | 
    
         
            +
                        return Signpost(
         
     | 
| 
      
 81 
     | 
    
         
            +
                            rel=LinkRel.collection, target=landing_page_url, media_type="text/html", context=link.target
         
     | 
| 
      
 82 
     | 
    
         
            +
                        )
         
     | 
| 
      
 83 
     | 
    
         
            +
                    case LinkRel.describedby:
         
     | 
| 
      
 84 
     | 
    
         
            +
                        if as_dict:
         
     | 
| 
      
 85 
     | 
    
         
            +
                            return {
         
     | 
| 
      
 86 
     | 
    
         
            +
                                "anchor": link.target,
         
     | 
| 
      
 87 
     | 
    
         
            +
                                str(LinkRel.describes): [{"href": landing_page_url, "type": "text/html"}],
         
     | 
| 
      
 88 
     | 
    
         
            +
                            }
         
     | 
| 
      
 89 
     | 
    
         
            +
                        return Signpost(rel=LinkRel.describes, target=landing_page_url, media_type="text/html", context=link.target)
         
     | 
| 
      
 90 
     | 
    
         
            +
                    case LinkRel.cite_as:
         
     | 
| 
      
 91 
     | 
    
         
            +
                        return None
         
     | 
| 
      
 92 
     | 
    
         
            +
                    # anchor is generated only for item & describedby, not for license
         
     | 
| 
      
 93 
     | 
    
         
            +
                    case _:
         
     | 
| 
      
 94 
     | 
    
         
            +
                        return None
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
            def anchor_signpost_link(signpost_link: Signpost, anchor_url: str) -> Signpost:
         
     | 
| 
      
 98 
     | 
    
         
            +
                """Add anchor to a signpost link."""
         
     | 
| 
      
 99 
     | 
    
         
            +
                signpost_link.context = AbsoluteURI(anchor_url)
         
     | 
| 
      
 100 
     | 
    
         
            +
                return signpost_link
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
            @overload
         
     | 
| 
      
 104 
     | 
    
         
            +
            def get_additional_links(
         
     | 
| 
      
 105 
     | 
    
         
            +
                list_of_signpost_links: list[Signpost], landing_page_url: str, as_dict: Literal[True] = True
         
     | 
| 
      
 106 
     | 
    
         
            +
            ) -> list[dict[str, Any]]: ...
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
            @overload
         
     | 
| 
      
 110 
     | 
    
         
            +
            def get_additional_links(
         
     | 
| 
      
 111 
     | 
    
         
            +
                list_of_signpost_links: list[Signpost], landing_page_url: str, as_dict: Literal[False]
         
     | 
| 
      
 112 
     | 
    
         
            +
            ) -> list[Signpost]: ...
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
            def get_additional_links(
         
     | 
| 
      
 116 
     | 
    
         
            +
                list_of_signpost_links: list[Signpost], landing_page_url: str, as_dict: bool = True
         
     | 
| 
      
 117 
     | 
    
         
            +
            ) -> list[Signpost] | list[dict[str, Any]]:
         
     | 
| 
      
 118 
     | 
    
         
            +
                """Create a list of additional links from a list of signpost links.
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 121 
     | 
    
         
            +
                    list_of_signpost_links: list of signpost link objects to be formatted
         
     | 
| 
      
 122 
     | 
    
         
            +
                    landing_page_url: landing page url
         
     | 
| 
      
 123 
     | 
    
         
            +
                    as_dict: if true, return a list of signpost link dicts, else return a list of Signpost link objects
         
     | 
| 
      
 124 
     | 
    
         
            +
                Returns: list of signpost link dicts or list of Signpost link objects
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
                """
         
     | 
| 
      
 127 
     | 
    
         
            +
                results = [
         
     | 
| 
      
 128 
     | 
    
         
            +
                    result
         
     | 
| 
      
 129 
     | 
    
         
            +
                    for signpost_link in list_of_signpost_links
         
     | 
| 
      
 130 
     | 
    
         
            +
                    if (
         
     | 
| 
      
 131 
     | 
    
         
            +
                        result := signpost_link_to_additional_link(
         
     | 
| 
      
 132 
     | 
    
         
            +
                            link=signpost_link, landing_page_url=landing_page_url, as_dict=as_dict
         
     | 
| 
      
 133 
     | 
    
         
            +
                        )
         
     | 
| 
      
 134 
     | 
    
         
            +
                    )
         
     | 
| 
      
 135 
     | 
    
         
            +
                    is not None
         
     | 
| 
      
 136 
     | 
    
         
            +
                ]
         
     | 
| 
      
 137 
     | 
    
         
            +
                if as_dict:
         
     | 
| 
      
 138 
     | 
    
         
            +
                    return cast("list[dict[str, Any]]", results)
         
     | 
| 
      
 139 
     | 
    
         
            +
                return cast("list[Signpost]", results)
         
     | 
| 
      
 140 
     | 
    
         
            +
             
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
            def list_of_signpost_links_to_http_header(links_list: list[Signpost]) -> str:
         
     | 
| 
      
 143 
     | 
    
         
            +
                """Create an HTTP Link header from a list of signpost links.
         
     | 
| 
      
 144 
     | 
    
         
            +
             
     | 
| 
      
 145 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 146 
     | 
    
         
            +
                    links_list: list of signpost link objects to be formatted
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
                Returns: signpost header with formatted links.
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                """
         
     | 
| 
      
 151 
     | 
    
         
            +
                links = [str(link)[6:] for link in links_list if str(link)[:6] == "Link: "]
         
     | 
| 
      
 152 
     | 
    
         
            +
                return f"Link: {', '.join(links)}"
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
             
     | 
| 
      
 155 
     | 
    
         
            +
            def create_linkset(datacite_dict: dict, record_item: RecordItem) -> str:
         
     | 
| 
      
 156 
     | 
    
         
            +
                """Create a linkset for the record item in the application/linkset format.
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 159 
     | 
    
         
            +
                    datacite_dict:  dictionary with datacite data
         
     | 
| 
      
 160 
     | 
    
         
            +
                    record_item: record item, for which signpost links should be generated
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
      
 162 
     | 
    
         
            +
                Returns: linkset in string format
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
                """
         
     | 
| 
      
 165 
     | 
    
         
            +
                landing_page_url = record_item.links.get("self_html")
         
     | 
| 
      
 166 
     | 
    
         
            +
                # just sanity check, we don't expect this to happen, not covered in tests
         
     | 
| 
      
 167 
     | 
    
         
            +
                if not landing_page_url:  # pragma: no cover
         
     | 
| 
      
 168 
     | 
    
         
            +
                    return ""
         
     | 
| 
      
 169 
     | 
    
         
            +
                landing_page_links = landing_page_signpost_links_list(datacite_dict, record_item, short=False)
         
     | 
| 
      
 170 
     | 
    
         
            +
                additional_links: list[Signpost] = get_additional_links(landing_page_links, landing_page_url, as_dict=False)
         
     | 
| 
      
 171 
     | 
    
         
            +
                anchored_links = [
         
     | 
| 
      
 172 
     | 
    
         
            +
                    anchor_signpost_link(signpost_link, landing_page_url) for signpost_link in landing_page_links
         
     | 
| 
      
 173 
     | 
    
         
            +
                ] + additional_links
         
     | 
| 
      
 174 
     | 
    
         
            +
                links = [str(link)[6:] for link in anchored_links if str(link)[:6] == "Link: "]
         
     | 
| 
      
 175 
     | 
    
         
            +
                return ", ".join(links)
         
     | 
| 
      
 176 
     | 
    
         
            +
             
     | 
| 
      
 177 
     | 
    
         
            +
             
     | 
| 
      
 178 
     | 
    
         
            +
            def create_linkset_json(datacite_dict: dict, record_item: RecordItem) -> dict[str, list[dict[str, Any]]]:
         
     | 
| 
      
 179 
     | 
    
         
            +
                """Create a linkset for the record item in the application/linkset+json format.
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 182 
     | 
    
         
            +
                    datacite_dict:  dictionary with datacite data
         
     | 
| 
      
 183 
     | 
    
         
            +
                    record_item: record item, for which signpost links should be generated
         
     | 
| 
      
 184 
     | 
    
         
            +
             
     | 
| 
      
 185 
     | 
    
         
            +
                Returns: linkset in JSON format
         
     | 
| 
      
 186 
     | 
    
         
            +
             
     | 
| 
      
 187 
     | 
    
         
            +
                """
         
     | 
| 
      
 188 
     | 
    
         
            +
                landing_page_url = record_item.links.get("self_html")
         
     | 
| 
      
 189 
     | 
    
         
            +
                # just sanity check, we don't expect this to happen, not covered in tests
         
     | 
| 
      
 190 
     | 
    
         
            +
                if not landing_page_url:  # pragma: no cover
         
     | 
| 
      
 191 
     | 
    
         
            +
                    return {}
         
     | 
| 
      
 192 
     | 
    
         
            +
                landing_page_links = landing_page_signpost_links_list(datacite_dict, record_item, short=False)
         
     | 
| 
      
 193 
     | 
    
         
            +
                dict_of_links_by_relation = defaultdict(list)
         
     | 
| 
      
 194 
     | 
    
         
            +
                for link in landing_page_links:
         
     | 
| 
      
 195 
     | 
    
         
            +
                    dict_of_links_by_relation[str(link.rel)].append(link)
         
     | 
| 
      
 196 
     | 
    
         
            +
                links_json = defaultdict(list)
         
     | 
| 
      
 197 
     | 
    
         
            +
                links_json["anchor"] = landing_page_url
         
     | 
| 
      
 198 
     | 
    
         
            +
             
     | 
| 
      
 199 
     | 
    
         
            +
                additional_links: list[dict[str, Any]] = get_additional_links(landing_page_links, landing_page_url)
         
     | 
| 
      
 200 
     | 
    
         
            +
                for link_relation_from_dict, list_of_links_for_relation in dict_of_links_by_relation.items():
         
     | 
| 
      
 201 
     | 
    
         
            +
                    for link in list_of_links_for_relation:
         
     | 
| 
      
 202 
     | 
    
         
            +
                        links_json[link_relation_from_dict].append(signpost_link_to_dict(link))
         
     | 
| 
      
 203 
     | 
    
         
            +
             
     | 
| 
      
 204 
     | 
    
         
            +
                return {"linkset": [dict(links_json), *[x for x in additional_links if x]]}
         
     | 
| 
      
 205 
     | 
    
         
            +
             
     | 
| 
      
 206 
     | 
    
         
            +
             
     | 
| 
      
 207 
     | 
    
         
            +
            def file_content_signpost_links_list(record_item: RecordItem) -> list[Signpost]:
         
     | 
| 
      
 208 
     | 
    
         
            +
                """Create a list of signpost links for the file content of the record item.
         
     | 
| 
      
 209 
     | 
    
         
            +
             
     | 
| 
      
 210 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 211 
     | 
    
         
            +
                    record_item: record item with the file to generate a signpost link for
         
     | 
| 
      
 212 
     | 
    
         
            +
             
     | 
| 
      
 213 
     | 
    
         
            +
                Returns: list with the signpost link for the file content
         
     | 
| 
      
 214 
     | 
    
         
            +
             
     | 
| 
      
 215 
     | 
    
         
            +
                """
         
     | 
| 
      
 216 
     | 
    
         
            +
                record = record_from_result(record_item)
         
     | 
| 
      
 217 
     | 
    
         
            +
                model = current_runtime.get_model_for_record(record)
         
     | 
| 
      
 218 
     | 
    
         
            +
                landing_page_url = model.ui_url(view_name="record_detail", pid_value=record.pid.pid_value)
         
     | 
| 
      
 219 
     | 
    
         
            +
                if not landing_page_url:  # pragma: no cover
         
     | 
| 
      
 220 
     | 
    
         
            +
                    return []
         
     | 
| 
      
 221 
     | 
    
         
            +
                return [
         
     | 
| 
      
 222 
     | 
    
         
            +
                    Signpost(
         
     | 
| 
      
 223 
     | 
    
         
            +
                        rel=LinkRel.linkset,
         
     | 
| 
      
 224 
     | 
    
         
            +
                        target=landing_page_url,
         
     | 
| 
      
 225 
     | 
    
         
            +
                        media_type="application/linkset",
         
     | 
| 
      
 226 
     | 
    
         
            +
                    ),
         
     | 
| 
      
 227 
     | 
    
         
            +
                    Signpost(
         
     | 
| 
      
 228 
     | 
    
         
            +
                        rel=LinkRel.linkset,
         
     | 
| 
      
 229 
     | 
    
         
            +
                        target=landing_page_url,
         
     | 
| 
      
 230 
     | 
    
         
            +
                        media_type="application/linkset+json",
         
     | 
| 
      
 231 
     | 
    
         
            +
                    ),
         
     | 
| 
      
 232 
     | 
    
         
            +
                    Signpost(
         
     | 
| 
      
 233 
     | 
    
         
            +
                        rel=LinkRel.collection,
         
     | 
| 
      
 234 
     | 
    
         
            +
                        target=landing_page_url,
         
     | 
| 
      
 235 
     | 
    
         
            +
                        media_type="text/html",
         
     | 
| 
      
 236 
     | 
    
         
            +
                    ),
         
     | 
| 
      
 237 
     | 
    
         
            +
                ]
         
     | 
| 
      
 238 
     | 
    
         
            +
             
     | 
| 
      
 239 
     | 
    
         
            +
             
     | 
| 
      
 240 
     | 
    
         
            +
            def export_format_signpost_links_list(record_item: RecordItem) -> list[Signpost]:
         
     | 
| 
      
 241 
     | 
    
         
            +
                """Create a list of signpost links for the export format of the record item.
         
     | 
| 
      
 242 
     | 
    
         
            +
             
     | 
| 
      
 243 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 244 
     | 
    
         
            +
                record_item: record item with the export format to generate a signpost link for
         
     | 
| 
      
 245 
     | 
    
         
            +
                code: code of the export format
         
     | 
| 
      
 246 
     | 
    
         
            +
             
     | 
| 
      
 247 
     | 
    
         
            +
                Returns: list with the signpost link for the export format
         
     | 
| 
      
 248 
     | 
    
         
            +
             
     | 
| 
      
 249 
     | 
    
         
            +
                """
         
     | 
| 
      
 250 
     | 
    
         
            +
                return [
         
     | 
| 
      
 251 
     | 
    
         
            +
                    Signpost(
         
     | 
| 
      
 252 
     | 
    
         
            +
                        rel=LinkRel.linkset,
         
     | 
| 
      
 253 
     | 
    
         
            +
                        target=record_item.links["self_html"],
         
     | 
| 
      
 254 
     | 
    
         
            +
                        media_type="application/linkset",
         
     | 
| 
      
 255 
     | 
    
         
            +
                    ),
         
     | 
| 
      
 256 
     | 
    
         
            +
                    Signpost(
         
     | 
| 
      
 257 
     | 
    
         
            +
                        rel=LinkRel.linkset,
         
     | 
| 
      
 258 
     | 
    
         
            +
                        target=record_item.links["self_html"],
         
     | 
| 
      
 259 
     | 
    
         
            +
                        media_type="application/linkset+json",
         
     | 
| 
      
 260 
     | 
    
         
            +
                    ),
         
     | 
| 
      
 261 
     | 
    
         
            +
                    Signpost(rel=LinkRel.describes, target=record_item.links["self_html"], media_type="text/html"),
         
     | 
| 
      
 262 
     | 
    
         
            +
                ]
         
     | 
| 
      
 263 
     | 
    
         
            +
             
     | 
| 
      
 264 
     | 
    
         
            +
             
     | 
| 
      
 265 
     | 
    
         
            +
            def landing_page_signpost_links_list(datacite_dict: dict, record_item: RecordItem, short: bool) -> list[Signpost]:
         
     | 
| 
      
 266 
     | 
    
         
            +
                """Create a list of signpost links for the landing page of the record item.
         
     | 
| 
      
 267 
     | 
    
         
            +
             
     | 
| 
      
 268 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 269 
     | 
    
         
            +
                    datacite_dict: dictionary with datacite data
         
     | 
| 
      
 270 
     | 
    
         
            +
                    record_item: record item, for which signpost links should be generated
         
     | 
| 
      
 271 
     | 
    
         
            +
                    short: If true, lists only the first three links for relations with greater count
         
     | 
| 
      
 272 
     | 
    
         
            +
             
     | 
| 
      
 273 
     | 
    
         
            +
                Returns: list of signpost links for the landing page
         
     | 
| 
      
 274 
     | 
    
         
            +
             
     | 
| 
      
 275 
     | 
    
         
            +
                """
         
     | 
| 
      
 276 
     | 
    
         
            +
                signposting_links: list[Signpost] = []
         
     | 
| 
      
 277 
     | 
    
         
            +
                record = record_from_result(record_item)
         
     | 
| 
      
 278 
     | 
    
         
            +
                record_data = record_item.data  # self.html
         
     | 
| 
      
 279 
     | 
    
         
            +
                record_files = record_data.get("files", {}).get("entries", {})
         
     | 
| 
      
 280 
     | 
    
         
            +
                model = current_runtime.get_model_for_record(record)
         
     | 
| 
      
 281 
     | 
    
         
            +
             
     | 
| 
      
 282 
     | 
    
         
            +
                # author - prvni tri
         
     | 
| 
      
 283 
     | 
    
         
            +
                data = datacite_dict["data"]
         
     | 
| 
      
 284 
     | 
    
         
            +
                attributes = data["attributes"]
         
     | 
| 
      
 285 
     | 
    
         
            +
                creators = attributes.get("creators", [])
         
     | 
| 
      
 286 
     | 
    
         
            +
                if short:
         
     | 
| 
      
 287 
     | 
    
         
            +
                    creators = creators[:3]
         
     | 
| 
      
 288 
     | 
    
         
            +
                for attribute in creators:
         
     | 
| 
      
 289 
     | 
    
         
            +
                    signposting_links.extend(
         
     | 
| 
      
 290 
     | 
    
         
            +
                        Signpost(rel=LinkRel.author, target=name_identifier["nameIdentifier"])
         
     | 
| 
      
 291 
     | 
    
         
            +
                        for name_identifier in attribute["nameIdentifiers"]
         
     | 
| 
      
 292 
     | 
    
         
            +
                    )
         
     | 
| 
      
 293 
     | 
    
         
            +
             
     | 
| 
      
 294 
     | 
    
         
            +
                # cite-as = DOI
         
     | 
| 
      
 295 
     | 
    
         
            +
                signposting_links.append(Signpost(rel=LinkRel.cite_as, target=urljoin("https://doi.org/", attributes.get("doi"))))
         
     | 
| 
      
 296 
     | 
    
         
            +
             
     | 
| 
      
 297 
     | 
    
         
            +
                # describedby
         
     | 
| 
      
 298 
     | 
    
         
            +
                for model_export in model.exports:
         
     | 
| 
      
 299 
     | 
    
         
            +
                    model_export_url = model.ui_url(
         
     | 
| 
      
 300 
     | 
    
         
            +
                        view_name="export", pid_value=record.pid.pid_value, export_format=model_export.code
         
     | 
| 
      
 301 
     | 
    
         
            +
                    )
         
     | 
| 
      
 302 
     | 
    
         
            +
                    # just sanity check, we don't expect this to happen, not covered in tests
         
     | 
| 
      
 303 
     | 
    
         
            +
                    if not model_export_url:  # pragma: no cover
         
     | 
| 
      
 304 
     | 
    
         
            +
                        continue
         
     | 
| 
      
 305 
     | 
    
         
            +
                    signposting_links.append(
         
     | 
| 
      
 306 
     | 
    
         
            +
                        Signpost(rel=LinkRel.describedby, target=model_export_url, media_type=model_export.mimetype)
         
     | 
| 
      
 307 
     | 
    
         
            +
                    )
         
     | 
| 
      
 308 
     | 
    
         
            +
             
     | 
| 
      
 309 
     | 
    
         
            +
                # item
         
     | 
| 
      
 310 
     | 
    
         
            +
                record_file_values = record_files.values()
         
     | 
| 
      
 311 
     | 
    
         
            +
                if short:
         
     | 
| 
      
 312 
     | 
    
         
            +
                    record_file_values = list(record_file_values)[:3]
         
     | 
| 
      
 313 
     | 
    
         
            +
                record_files_url = record_item.links.get("files")
         
     | 
| 
      
 314 
     | 
    
         
            +
                if record_files_url:
         
     | 
| 
      
 315 
     | 
    
         
            +
                    signposting_links.extend(
         
     | 
| 
      
 316 
     | 
    
         
            +
                        Signpost(
         
     | 
| 
      
 317 
     | 
    
         
            +
                            rel=LinkRel.item,
         
     | 
| 
      
 318 
     | 
    
         
            +
                            media_type=record_file.get("mimetype"),
         
     | 
| 
      
 319 
     | 
    
         
            +
                            target=f"{record_files_url}/{record_file.get('key')}",
         
     | 
| 
      
 320 
     | 
    
         
            +
                        )
         
     | 
| 
      
 321 
     | 
    
         
            +
                        for record_file in record_file_values
         
     | 
| 
      
 322 
     | 
    
         
            +
                    )
         
     | 
| 
      
 323 
     | 
    
         
            +
             
     | 
| 
      
 324 
     | 
    
         
            +
                # license
         
     | 
| 
      
 325 
     | 
    
         
            +
                for attribute in attributes.get("rightsList"):
         
     | 
| 
      
 326 
     | 
    
         
            +
                    # check for schemeUri, rightsIdentifier and 'rightsIdentifierScheme' == SPDX, fallback rightsUri, else nothing
         
     | 
| 
      
 327 
     | 
    
         
            +
                    license_url = attribute.get("rightsUri")
         
     | 
| 
      
 328 
     | 
    
         
            +
                    if (
         
     | 
| 
      
 329 
     | 
    
         
            +
                        attribute.get("schemeUri")
         
     | 
| 
      
 330 
     | 
    
         
            +
                        and attribute.get("rightsIdentifier")
         
     | 
| 
      
 331 
     | 
    
         
            +
                        and attribute.get("rightsIdentifierScheme") == "SPDX"
         
     | 
| 
      
 332 
     | 
    
         
            +
                    ):
         
     | 
| 
      
 333 
     | 
    
         
            +
                        license_url = urljoin(attribute.get("schemeUri"), attribute.get("rightsIdentifier"))
         
     | 
| 
      
 334 
     | 
    
         
            +
                    signposting_links.append(Signpost(rel=LinkRel.license, target=license_url))
         
     | 
| 
      
 335 
     | 
    
         
            +
             
     | 
| 
      
 336 
     | 
    
         
            +
                # type
         
     | 
| 
      
 337 
     | 
    
         
            +
                schema_org = attributes.get("types", {}).get("schemaOrg")
         
     | 
| 
      
 338 
     | 
    
         
            +
                if schema_org:
         
     | 
| 
      
 339 
     | 
    
         
            +
                    resource_type_url = "https://schema.org/" + schema_org
         
     | 
| 
      
 340 
     | 
    
         
            +
                    signposting_links.append(Signpost(rel=LinkRel.type, target=resource_type_url))
         
     | 
| 
      
 341 
     | 
    
         
            +
                signposting_links.append(Signpost(rel=LinkRel.type, target="https://schema.org/AboutPage"))
         
     | 
| 
      
 342 
     | 
    
         
            +
             
     | 
| 
      
 343 
     | 
    
         
            +
                return signposting_links
         
     | 
| 
      
 344 
     | 
    
         
            +
             
     | 
| 
      
 345 
     | 
    
         
            +
             
     | 
| 
      
 346 
     | 
    
         
            +
            def record_to_linkset(record_item: RecordItem) -> str:
         
     | 
| 
      
 347 
     | 
    
         
            +
                """Create a linkset from the record item. Get datacite to build linkset from model exports."""
         
     | 
| 
      
 348 
     | 
    
         
            +
                record = record_from_result(record_item)
         
     | 
| 
      
 349 
     | 
    
         
            +
                model = current_runtime.get_model_for_record(record)
         
     | 
| 
      
 350 
     | 
    
         
            +
                datacite_export = model.get_export_by_mimetype("application/vnd.datacite.datacite+json")
         
     | 
| 
      
 351 
     | 
    
         
            +
                if not datacite_export:
         
     | 
| 
      
 352 
     | 
    
         
            +
                    return ""
         
     | 
| 
      
 353 
     | 
    
         
            +
                datacite_dict = datacite_export.serializer.serialize_object(record)
         
     | 
| 
      
 354 
     | 
    
         
            +
                return create_linkset(datacite_dict, record_item)
         
     | 
| 
      
 355 
     | 
    
         
            +
             
     | 
| 
      
 356 
     | 
    
         
            +
             
     | 
| 
      
 357 
     | 
    
         
            +
            def record_to_json_linkset(record_item: RecordItem) -> dict[str, list[dict[str, Any]]]:
         
     | 
| 
      
 358 
     | 
    
         
            +
                """Create a JSON linkset from the record item. Get datacite to build linkset from model exports."""
         
     | 
| 
      
 359 
     | 
    
         
            +
                record = record_from_result(record_item)
         
     | 
| 
      
 360 
     | 
    
         
            +
                model = current_runtime.get_model_for_record(record)
         
     | 
| 
      
 361 
     | 
    
         
            +
                datacite_export = model.get_export_by_mimetype("application/vnd.datacite.datacite+json")
         
     | 
| 
      
 362 
     | 
    
         
            +
                if not datacite_export:
         
     | 
| 
      
 363 
     | 
    
         
            +
                    return {}
         
     | 
| 
      
 364 
     | 
    
         
            +
                datacite_dict = datacite_export.serializer.serialize_object(record)
         
     | 
| 
      
 365 
     | 
    
         
            +
                return create_linkset_json(datacite_dict, record_item)
         
     | 
| 
         @@ -1,6 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            Metadata-Version: 2.4
         
     | 
| 
       2 
2 
     | 
    
         
             
            Name: oarepo-runtime
         
     | 
| 
       3 
     | 
    
         
            -
            Version: 2.0.0. 
     | 
| 
      
 3 
     | 
    
         
            +
            Version: 2.0.0.dev39
         
     | 
| 
       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
         
     | 
| 
         @@ -9,6 +9,7 @@ Requires-Python: <3.14,>=3.13 
     | 
|
| 
       9 
9 
     | 
    
         
             
            Requires-Dist: langcodes>=3.5.0
         
     | 
| 
       10 
10 
     | 
    
         
             
            Requires-Dist: oarepo-invenio-typing-stubs>=0.1.0
         
     | 
| 
       11 
11 
     | 
    
         
             
            Requires-Dist: oarepo[rdm,tests]<15,>=14
         
     | 
| 
      
 12 
     | 
    
         
            +
            Requires-Dist: signposting>=0.9.9
         
     | 
| 
       12 
13 
     | 
    
         
             
            Provides-Extra: dev
         
     | 
| 
       13 
14 
     | 
    
         
             
            Requires-Dist: pytest>=7.1.2; extra == 'dev'
         
     | 
| 
       14 
15 
     | 
    
         
             
            Provides-Extra: oarepo14
         
     | 
| 
         @@ -1,14 +1,14 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            oarepo_runtime/__init__.py,sha256= 
     | 
| 
       2 
     | 
    
         
            -
            oarepo_runtime/api.py,sha256= 
     | 
| 
      
 1 
     | 
    
         
            +
            oarepo_runtime/__init__.py,sha256=Hq07N9-oKqUJGYTjwG4TvOXZT2Usdd3GJ-ySd7Y3Wrw,686
         
     | 
| 
      
 2 
     | 
    
         
            +
            oarepo_runtime/api.py,sha256=hEYmBbqxk0DRfLGikSx4NMMmyDYxUKRAVP1z678iIOw,15000
         
     | 
| 
       3 
3 
     | 
    
         
             
            oarepo_runtime/config.py,sha256=RUEPFn_5bKp9Wb0OY-Fb3VK30m35vF5IsLjYaQHhP3g,3838
         
     | 
| 
       4 
     | 
    
         
            -
            oarepo_runtime/ext.py,sha256= 
     | 
| 
      
 4 
     | 
    
         
            +
            oarepo_runtime/ext.py,sha256=OdhT4gGKK2LpNcJ30cHndCxWZgeHdAk1Ev8bISeX8Qk,9321
         
     | 
| 
       5 
5 
     | 
    
         
             
            oarepo_runtime/proxies.py,sha256=x8Y1iTP8QIzSI67s90VR0_5fvXuT1xlJXtAHsaoXFwg,903
         
     | 
| 
       6 
6 
     | 
    
         
             
            oarepo_runtime/py.typed,sha256=RznSCjXReEUI9zkmD25E8XniG_MvPpLBF6MyNZA8MmE,42
         
     | 
| 
       7 
7 
     | 
    
         
             
            oarepo_runtime/typing.py,sha256=VtINHm4BZ5OJ4KdRAwnFXPZiAEgPRIYTtPC9fIzK1bU,1876
         
     | 
| 
       8 
8 
     | 
    
         
             
            oarepo_runtime/cli/__init__.py,sha256=H7GOeOBf0udgKWOdlAQswIMvRrD8BwcEjOVxIqP0Suw,731
         
     | 
| 
       9 
9 
     | 
    
         
             
            oarepo_runtime/cli/search.py,sha256=4fHkrjltUUPVUzJiuWaiWxTk62rIYxal3_3jRsZVMmI,1175
         
     | 
| 
       10 
10 
     | 
    
         
             
            oarepo_runtime/info/__init__.py,sha256=qRG3mSyoiw7sKm9StiuBJs6l15HrdAQ4sphsAQsJtQc,336
         
     | 
| 
       11 
     | 
    
         
            -
            oarepo_runtime/info/views.py,sha256= 
     | 
| 
      
 11 
     | 
    
         
            +
            oarepo_runtime/info/views.py,sha256=ObeRjbuH5_eKMOkVfT3O5EhPLIQ0HuCLD3fXSEv2z7U,16987
         
     | 
| 
       12 
12 
     | 
    
         
             
            oarepo_runtime/records/__init__.py,sha256=AbWzmVCY7MhrpdEeI0e3lKzeugPMUSo8T08-NBVeig4,339
         
     | 
| 
       13 
13 
     | 
    
         
             
            oarepo_runtime/records/drafts.py,sha256=b45ROjd9lwy6ratrpAruimcKvQmJradk5JgILoBAHmY,1965
         
     | 
| 
       14 
14 
     | 
    
         
             
            oarepo_runtime/records/mapping.py,sha256=fn6M208axxBqHtRV6qKQukwUw1z0hq_KF4qfuB2rr98,2630
         
     | 
| 
         @@ -18,9 +18,11 @@ oarepo_runtime/records/systemfields/base.py,sha256=EWSdVsWePkdwksRQ9yaMMk9Mrhicw 
     | 
|
| 
       18 
18 
     | 
    
         
             
            oarepo_runtime/records/systemfields/custom_fields.py,sha256=PEoaCEnvanysFQAaqTtD9-VwaBmnFkoP2pmpCl9ZFfI,2237
         
     | 
| 
       19 
19 
     | 
    
         
             
            oarepo_runtime/records/systemfields/mapping.py,sha256=GcNp_-Ho3G8nk4-SXgoWWk_IPdGsM0LZ-DBl5fnYJvE,1699
         
     | 
| 
       20 
20 
     | 
    
         
             
            oarepo_runtime/records/systemfields/publication_status.py,sha256=5D8L_-Wsf-Or-Er7EhuOnwzXfGY28TRPTg1XmMEKxM8,1995
         
     | 
| 
      
 21 
     | 
    
         
            +
            oarepo_runtime/records/systemfields/relations.py,sha256=EyFTpdglkRCeCtNg3lXTh3H2_F3zTTjttjMQmuwt1M0,12240
         
     | 
| 
       21 
22 
     | 
    
         
             
            oarepo_runtime/records/systemfields/selectors.py,sha256=ijVDwAXaXTV5NtcXsrALkhddgCogLNe2eEscFr23qyg,1656
         
     | 
| 
       22 
23 
     | 
    
         
             
            oarepo_runtime/resources/__init__.py,sha256=voynQULXoOEviADkbOpekMphZPTAz4IOTg5BF9xPwTM,453
         
     | 
| 
       23 
24 
     | 
    
         
             
            oarepo_runtime/resources/config.py,sha256=Lbx1QPWAJ8z1truhYntbnhGGWp2OCcwqKm6BuvPJNT0,1330
         
     | 
| 
      
 25 
     | 
    
         
            +
            oarepo_runtime/resources/signposting/__init__.py,sha256=USztM57sVm1LWxACxt6o8cy3VZS0Kz-MYrjG8Jm-Ock,14959
         
     | 
| 
       24 
26 
     | 
    
         
             
            oarepo_runtime/services/__init__.py,sha256=OGtBgEeaDTyk2RPDNXuKbU9_7egFBZr42SM0gN5FrF4,341
         
     | 
| 
       25 
27 
     | 
    
         
             
            oarepo_runtime/services/generators.py,sha256=8Z2QGzob4c2vaaNqhcMZsRybmwtOt30Plgf3EFmcJXw,4622
         
     | 
| 
       26 
28 
     | 
    
         
             
            oarepo_runtime/services/results.py,sha256=EwMW1ed7u6uozgOLZpFa07-NKC89hJlHaVSD8-D5ibU,7211
         
     | 
| 
         @@ -42,8 +44,8 @@ oarepo_runtime/services/schema/__init__.py,sha256=jgAPI_uKC6Ug4KQWnwQVg3-aNaw-eH 
     | 
|
| 
       42 
44 
     | 
    
         
             
            oarepo_runtime/services/schema/i18n.py,sha256=9D1zOQaPKAnYzejB0vO-m2BJYnam0N0Lrq4jID7twfE,3174
         
     | 
| 
       43 
45 
     | 
    
         
             
            oarepo_runtime/services/schema/i18n_ui.py,sha256=DbusphhGDeaobTt4nuwNgKZ6Houlu4Sv3SuMGkdjRRY,3582
         
     | 
| 
       44 
46 
     | 
    
         
             
            oarepo_runtime/services/schema/ui.py,sha256=Y_jBO-fowkpOgceWz8aqJSJAUiAnKLGSIuNpjNLnp8Q,4612
         
     | 
| 
       45 
     | 
    
         
            -
            oarepo_runtime-2.0.0. 
     | 
| 
       46 
     | 
    
         
            -
            oarepo_runtime-2.0.0. 
     | 
| 
       47 
     | 
    
         
            -
            oarepo_runtime-2.0.0. 
     | 
| 
       48 
     | 
    
         
            -
            oarepo_runtime-2.0.0. 
     | 
| 
       49 
     | 
    
         
            -
            oarepo_runtime-2.0.0. 
     | 
| 
      
 47 
     | 
    
         
            +
            oarepo_runtime-2.0.0.dev39.dist-info/METADATA,sha256=yqEwOh9uNtI0K20bSvj-GDXDZJjhZj8comcClUjJaPA,4660
         
     | 
| 
      
 48 
     | 
    
         
            +
            oarepo_runtime-2.0.0.dev39.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
         
     | 
| 
      
 49 
     | 
    
         
            +
            oarepo_runtime-2.0.0.dev39.dist-info/entry_points.txt,sha256=rOfs8R1oXFN_dLH9zAZ6ydkvr83mDajegc6NBIRsCMQ,318
         
     | 
| 
      
 50 
     | 
    
         
            +
            oarepo_runtime-2.0.0.dev39.dist-info/licenses/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
         
     | 
| 
      
 51 
     | 
    
         
            +
            oarepo_runtime-2.0.0.dev39.dist-info/RECORD,,
         
     | 
| 
         
            File without changes
         
     | 
    
        {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev39.dist-info}/entry_points.txt
    RENAMED
    
    | 
         
            File without changes
         
     | 
    
        {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev39.dist-info}/licenses/LICENSE
    RENAMED
    
    | 
         
            File without changes
         
     |