oarepo-runtime 2.0.0.dev37__py3-none-any.whl → 2.0.0.dev38__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/info/views.py +2 -3
 - oarepo_runtime/records/systemfields/relations.py +307 -0
 - {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev38.dist-info}/METADATA +1 -1
 - {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev38.dist-info}/RECORD +8 -7
 - {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev38.dist-info}/WHEEL +0 -0
 - {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev38.dist-info}/entry_points.txt +0 -0
 - {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev38.dist-info}/licenses/LICENSE +0 -0
 
    
        oarepo_runtime/__init__.py
    CHANGED
    
    
    
        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."""
         
     | 
| 
         @@ -1,4 +1,4 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            oarepo_runtime/__init__.py,sha256= 
     | 
| 
      
 1 
     | 
    
         
            +
            oarepo_runtime/__init__.py,sha256=hHdSN2hRXm0UHFuHSaaokxvEIdJtIoobD_jdybp_vKY,686
         
     | 
| 
       2 
2 
     | 
    
         
             
            oarepo_runtime/api.py,sha256=6AHyFnf0Cg1nfhGaQFMPjynVvrp1UURo8vX5vb5LvC4,14169
         
     | 
| 
       3 
3 
     | 
    
         
             
            oarepo_runtime/config.py,sha256=RUEPFn_5bKp9Wb0OY-Fb3VK30m35vF5IsLjYaQHhP3g,3838
         
     | 
| 
       4 
4 
     | 
    
         
             
            oarepo_runtime/ext.py,sha256=hA_OmJJHum6W28iPkjxvFrKHzFwi_Ki_0Fy2Mgzl7UQ,8851
         
     | 
| 
         @@ -8,7 +8,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,6 +18,7 @@ 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
         
     | 
| 
         @@ -42,8 +43,8 @@ oarepo_runtime/services/schema/__init__.py,sha256=jgAPI_uKC6Ug4KQWnwQVg3-aNaw-eH 
     | 
|
| 
       42 
43 
     | 
    
         
             
            oarepo_runtime/services/schema/i18n.py,sha256=9D1zOQaPKAnYzejB0vO-m2BJYnam0N0Lrq4jID7twfE,3174
         
     | 
| 
       43 
44 
     | 
    
         
             
            oarepo_runtime/services/schema/i18n_ui.py,sha256=DbusphhGDeaobTt4nuwNgKZ6Houlu4Sv3SuMGkdjRRY,3582
         
     | 
| 
       44 
45 
     | 
    
         
             
            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. 
     | 
| 
      
 46 
     | 
    
         
            +
            oarepo_runtime-2.0.0.dev38.dist-info/METADATA,sha256=avdajb5WZmRJ1u6EYEABUTGmJiDdGQjPn-lOSDD9aEg,4626
         
     | 
| 
      
 47 
     | 
    
         
            +
            oarepo_runtime-2.0.0.dev38.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
         
     | 
| 
      
 48 
     | 
    
         
            +
            oarepo_runtime-2.0.0.dev38.dist-info/entry_points.txt,sha256=rOfs8R1oXFN_dLH9zAZ6ydkvr83mDajegc6NBIRsCMQ,318
         
     | 
| 
      
 49 
     | 
    
         
            +
            oarepo_runtime-2.0.0.dev38.dist-info/licenses/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
         
     | 
| 
      
 50 
     | 
    
         
            +
            oarepo_runtime-2.0.0.dev38.dist-info/RECORD,,
         
     | 
| 
         
            File without changes
         
     | 
    
        {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev38.dist-info}/entry_points.txt
    RENAMED
    
    | 
         
            File without changes
         
     | 
    
        {oarepo_runtime-2.0.0.dev37.dist-info → oarepo_runtime-2.0.0.dev38.dist-info}/licenses/LICENSE
    RENAMED
    
    | 
         
            File without changes
         
     |