oarepo-runtime 1.5.6__py3-none-any.whl → 1.5.7__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/cli/fixtures.py +6 -3
- oarepo_runtime/datastreams/fixtures.py +15 -6
- oarepo_runtime/ext.py +3 -5
- oarepo_runtime/info/__init__.py +0 -0
- oarepo_runtime/info/views.py +302 -0
- oarepo_runtime/records/systemfields/mapping.py +4 -0
- oarepo_runtime/records/systemfields/synthetic.py +79 -0
- oarepo_runtime/services/custom_fields/mappings.py +9 -3
- oarepo_runtime/services/results.py +5 -1
- oarepo_runtime/utils/functools.py +8 -0
- {oarepo_runtime-1.5.6.dist-info → oarepo_runtime-1.5.7.dist-info}/METADATA +1 -1
- {oarepo_runtime-1.5.6.dist-info → oarepo_runtime-1.5.7.dist-info}/RECORD +16 -12
- {oarepo_runtime-1.5.6.dist-info → oarepo_runtime-1.5.7.dist-info}/entry_points.txt +3 -0
- {oarepo_runtime-1.5.6.dist-info → oarepo_runtime-1.5.7.dist-info}/LICENSE +0 -0
- {oarepo_runtime-1.5.6.dist-info → oarepo_runtime-1.5.7.dist-info}/WHEEL +0 -0
- {oarepo_runtime-1.5.6.dist-info → oarepo_runtime-1.5.7.dist-info}/top_level.txt +0 -0
    
        oarepo_runtime/cli/fixtures.py
    CHANGED
    
    | @@ -48,6 +48,9 @@ def load( | |
| 48 48 | 
             
                else:
         | 
| 49 49 | 
             
                    callback = fixtures_asynchronous_callback.s()
         | 
| 50 50 |  | 
| 51 | 
            +
                if fixture_dir:
         | 
| 52 | 
            +
                    system_fixtures = False
         | 
| 53 | 
            +
             | 
| 51 54 | 
             
                with current_app.wsgi_app.mounts["/api"].app_context():
         | 
| 52 55 | 
             
                    load_fixtures(
         | 
| 53 56 | 
             
                        fixture_dir,
         | 
| @@ -56,9 +59,9 @@ def load( | |
| 56 59 | 
             
                        system_fixtures=system_fixtures,
         | 
| 57 60 | 
             
                        callback=callback,
         | 
| 58 61 | 
             
                        batch_size=bulk_size,
         | 
| 59 | 
            -
                        datastreams_impl= | 
| 60 | 
            -
             | 
| 61 | 
            -
                         | 
| 62 | 
            +
                        datastreams_impl=(
         | 
| 63 | 
            +
                            AsynchronousDataStream if on_background else SynchronousDataStream
         | 
| 64 | 
            +
                        ),
         | 
| 62 65 | 
             
                    )
         | 
| 63 66 | 
             
                    if not on_background:
         | 
| 64 67 | 
             
                        _show_stats(callback, "Load fixtures")
         | 
| @@ -57,12 +57,20 @@ def load_fixtures( | |
| 57 57 | 
             
                    )
         | 
| 58 58 |  | 
| 59 59 | 
             
                if system_fixtures:
         | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
                         | 
| 64 | 
            -
             | 
| 65 | 
            -
                         | 
| 60 | 
            +
             | 
| 61 | 
            +
                    def get_priority(name):
         | 
| 62 | 
            +
                        match = re.match(r"(\d+)-", name)
         | 
| 63 | 
            +
                        if match:
         | 
| 64 | 
            +
                            return -int(match.group(1))
         | 
| 65 | 
            +
                        return 0
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    entry_points = list(
         | 
| 68 | 
            +
                        (get_priority(r.name), r.name, r)
         | 
| 69 | 
            +
                        for r in pkg_resources.iter_entry_points("oarepo.fixtures")
         | 
| 70 | 
            +
                    )
         | 
| 71 | 
            +
                    entry_points.sort(key=lambda x: x[:2])
         | 
| 72 | 
            +
                    for r in entry_points:
         | 
| 73 | 
            +
                        pkg = r[2].load()
         | 
| 66 74 | 
             
                        pkg_fixture_dir = Path(pkg.__file__)
         | 
| 67 75 | 
             
                        if pkg_fixture_dir.is_file():
         | 
| 68 76 | 
             
                            pkg_fixture_dir = pkg_fixture_dir.parent
         | 
| @@ -90,6 +98,7 @@ def _load_fixtures_from_catalogue( | |
| 90 98 | 
             
                        continue
         | 
| 91 99 | 
             
                    if any(x.match(catalogue_datastream.stream_name) for x in exclude):
         | 
| 92 100 | 
             
                        continue
         | 
| 101 | 
            +
             | 
| 93 102 | 
             
                    fixtures.add(catalogue_datastream.stream_name)
         | 
| 94 103 |  | 
| 95 104 | 
             
                    datastream = datastreams_impl(
         | 
    
        oarepo_runtime/ext.py
    CHANGED
    
    | @@ -28,9 +28,9 @@ class OARepoRuntime(object): | |
| 28 28 |  | 
| 29 29 | 
             
                    for k in ext_config.OAREPO_PERMISSIONS_PRESETS:
         | 
| 30 30 | 
             
                        if k not in app.config["OAREPO_PERMISSIONS_PRESETS"]:
         | 
| 31 | 
            -
                            app.config["OAREPO_PERMISSIONS_PRESETS"][
         | 
| 32 | 
            -
                                k
         | 
| 33 | 
            -
                             | 
| 31 | 
            +
                            app.config["OAREPO_PERMISSIONS_PRESETS"][k] = (
         | 
| 32 | 
            +
                                ext_config.OAREPO_PERMISSIONS_PRESETS[k]
         | 
| 33 | 
            +
                            )
         | 
| 34 34 |  | 
| 35 35 | 
             
                    for k in dir(ext_config):
         | 
| 36 36 | 
             
                        if k == "DEFAULT_DATASTREAMS_EXCLUDES":
         | 
| @@ -53,5 +53,3 @@ class OARepoRuntime(object): | |
| 53 53 | 
             
                    for val_k, val_value in source.items():
         | 
| 54 54 | 
             
                        if val_k not in target:
         | 
| 55 55 | 
             
                            target[val_k] = val_value
         | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 
            File without changes
         | 
| @@ -0,0 +1,302 @@ | |
| 1 | 
            +
            import json
         | 
| 2 | 
            +
            import logging
         | 
| 3 | 
            +
            import re
         | 
| 4 | 
            +
            from functools import cached_property
         | 
| 5 | 
            +
            from urllib.parse import urljoin
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            import importlib_metadata
         | 
| 8 | 
            +
            import importlib_resources
         | 
| 9 | 
            +
            import marshmallow as ma
         | 
| 10 | 
            +
            from flask import current_app, request, url_for
         | 
| 11 | 
            +
            from flask_resources import (
         | 
| 12 | 
            +
                Resource,
         | 
| 13 | 
            +
                ResourceConfig,
         | 
| 14 | 
            +
                from_conf,
         | 
| 15 | 
            +
                request_parser,
         | 
| 16 | 
            +
                resource_requestctx,
         | 
| 17 | 
            +
                response_handler,
         | 
| 18 | 
            +
                route,
         | 
| 19 | 
            +
            )
         | 
| 20 | 
            +
            from flask_restful import abort
         | 
| 21 | 
            +
            from invenio_base.utils import obj_or_import_string
         | 
| 22 | 
            +
            from invenio_jsonschemas import current_jsonschemas
         | 
| 23 | 
            +
            from invenio_records_resources.proxies import current_service_registry
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            logger = logging.getLogger("oarepo_runtime.info")
         | 
| 26 | 
            +
             | 
| 27 | 
            +
             | 
| 28 | 
            +
            class InfoConfig(ResourceConfig):
         | 
| 29 | 
            +
                blueprint_name = "oarepo_runtime_info"
         | 
| 30 | 
            +
                url_prefix = "/.well-known/repository"
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                schema_view_args = {"schema": ma.fields.Str()}
         | 
| 33 | 
            +
                model_view_args = {"model": ma.fields.Str()}
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def __init__(self, app):
         | 
| 36 | 
            +
                    self.app = app
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                @cached_property
         | 
| 39 | 
            +
                def components(self):
         | 
| 40 | 
            +
                    return tuple(
         | 
| 41 | 
            +
                        obj_or_import_string(x)
         | 
| 42 | 
            +
                        for x in self.app.config.get("INFO_ENDPOINT_COMPONENTS", [])
         | 
| 43 | 
            +
                    )
         | 
| 44 | 
            +
             | 
| 45 | 
            +
             | 
| 46 | 
            +
            schema_view_args = request_parser(from_conf("schema_view_args"), location="view_args")
         | 
| 47 | 
            +
            model_view_args = request_parser(from_conf("model_view_args"), location="view_args")
         | 
| 48 | 
            +
             | 
| 49 | 
            +
             | 
| 50 | 
            +
            class InfoResource(Resource):
         | 
| 51 | 
            +
                def create_url_rules(self):
         | 
| 52 | 
            +
                    return [
         | 
| 53 | 
            +
                        route("GET", "/", self.repository),
         | 
| 54 | 
            +
                        route("GET", "/models", self.models),
         | 
| 55 | 
            +
                        route("GET", "/schema/<path:schema>", self.schema),
         | 
| 56 | 
            +
                        route("GET", "/models/<model>", self.model),
         | 
| 57 | 
            +
                    ]
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                @cached_property
         | 
| 60 | 
            +
                def components(self):
         | 
| 61 | 
            +
                    return [x(self) for x in self.config.components]
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                @response_handler()
         | 
| 64 | 
            +
                def repository(self):
         | 
| 65 | 
            +
                    """Repository endpoint."""
         | 
| 66 | 
            +
                    ret = {
         | 
| 67 | 
            +
                        "name": current_app.config.get("THEME_SITENAME", ""),
         | 
| 68 | 
            +
                        "description": current_app.config.get("REPOSITORY_DESCRIPTION", ""),
         | 
| 69 | 
            +
                        "version": get_package_version("repo"),
         | 
| 70 | 
            +
                        "invenio_version": get_package_version("oarepo"),
         | 
| 71 | 
            +
                        "transfers": [
         | 
| 72 | 
            +
                            "local-file",
         | 
| 73 | 
            +
                            "url-fetch",
         | 
| 74 | 
            +
                            # TODO: where to get these? (permissions?)
         | 
| 75 | 
            +
                            # "direct-s3",
         | 
| 76 | 
            +
                        ],
         | 
| 77 | 
            +
                        "links": {
         | 
| 78 | 
            +
                            "self": url_for(request.endpoint, _external=True),
         | 
| 79 | 
            +
                            "models": url_for("oarepo_runtime_info.models", _external=True),
         | 
| 80 | 
            +
                        },
         | 
| 81 | 
            +
                    }
         | 
| 82 | 
            +
                    self.call_components("repository", data=ret)
         | 
| 83 | 
            +
                    return ret, 200
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                @response_handler(many=True)
         | 
| 86 | 
            +
                def models(self):
         | 
| 87 | 
            +
                    data = []
         | 
| 88 | 
            +
                    # iterate entrypoint oarepo.models
         | 
| 89 | 
            +
                    for model in importlib_metadata.entry_points().select(group="oarepo.models"):
         | 
| 90 | 
            +
                        package_name, file_name = model.value.split(":")
         | 
| 91 | 
            +
                        model_data = json.loads(
         | 
| 92 | 
            +
                            importlib_resources.files(package_name).joinpath(file_name).read_text()
         | 
| 93 | 
            +
                        )
         | 
| 94 | 
            +
                        model_data = model_data["model"]
         | 
| 95 | 
            +
                        if model_data["type"] != "model":
         | 
| 96 | 
            +
                            continue
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                        service = self._get_service(model_data)
         | 
| 99 | 
            +
                        if not service:
         | 
| 100 | 
            +
                            continue
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                        model_features = self._get_model_features(model_data)
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                        links = {
         | 
| 105 | 
            +
                            "api": self._get_model_api_endpoint(model_data),
         | 
| 106 | 
            +
                            "html": self._get_model_html_endpoint(model_data),
         | 
| 107 | 
            +
                            "schema": self._get_model_schema_endpoint(model_data),
         | 
| 108 | 
            +
                            "model": self._get_model_model_endpoint(model.name),
         | 
| 109 | 
            +
                            # "openapi": url_for(self._get_model_openapi_endpoint(model_data), _external=True)
         | 
| 110 | 
            +
                        }
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                        links["published"] = links["api"]
         | 
| 113 | 
            +
                        if "drafts" in model_features:
         | 
| 114 | 
            +
                            links["drafts"] = self._get_model_draft_endpoint(model_data)
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                        data.append(
         | 
| 117 | 
            +
                            {
         | 
| 118 | 
            +
                                "name": model_data.get(
         | 
| 119 | 
            +
                                    "model-name", model_data.get("module", {}).get("base", "")
         | 
| 120 | 
            +
                                ).lower(),
         | 
| 121 | 
            +
                                "description": model_data.get("model-description", ""),
         | 
| 122 | 
            +
                                "version": model_data["json-schema-settings"]["version"],
         | 
| 123 | 
            +
                                "features": model_features,
         | 
| 124 | 
            +
                                "links": links,
         | 
| 125 | 
            +
                                # TODO: we also need to get previous schema versions here if we support
         | 
| 126 | 
            +
                                # multiple version of the same schema at the same time
         | 
| 127 | 
            +
                                "schemas": self._get_model_schemas(service),
         | 
| 128 | 
            +
                            }
         | 
| 129 | 
            +
                        )
         | 
| 130 | 
            +
                    self.call_components("repository", data=data)
         | 
| 131 | 
            +
                    return data, 200
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                @schema_view_args
         | 
| 134 | 
            +
                @response_handler()
         | 
| 135 | 
            +
                def schema(self):
         | 
| 136 | 
            +
                    schema = resource_requestctx.view_args["schema"]
         | 
| 137 | 
            +
                    return current_jsonschemas.get_schema(schema, resolved=True), 200
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                @model_view_args
         | 
| 140 | 
            +
                @response_handler()
         | 
| 141 | 
            +
                def model(self):
         | 
| 142 | 
            +
                    model = resource_requestctx.view_args["model"]
         | 
| 143 | 
            +
                    for _model in importlib_metadata.entry_points().select(
         | 
| 144 | 
            +
                        group="oarepo.models", name=model
         | 
| 145 | 
            +
                    ):
         | 
| 146 | 
            +
                        package_name, file_name = _model.value.split(":")
         | 
| 147 | 
            +
                        model_data = json.loads(
         | 
| 148 | 
            +
                            importlib_resources.files(package_name).joinpath(file_name).read_text()
         | 
| 149 | 
            +
                        )
         | 
| 150 | 
            +
                        return self._remove_implementation_details_from_model(model_data), 200
         | 
| 151 | 
            +
                    abort(404)
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                IMPLEMENTATION_DETAILS = re.compile(
         | 
| 154 | 
            +
                    r"""
         | 
| 155 | 
            +
            ^(
         | 
| 156 | 
            +
              class | 
         | 
| 157 | 
            +
              .*-class |
         | 
| 158 | 
            +
              base-classes |
         | 
| 159 | 
            +
              .*-base-classes |
         | 
| 160 | 
            +
              module |
         | 
| 161 | 
            +
              generate |
         | 
| 162 | 
            +
              imports |
         | 
| 163 | 
            +
              extra-code |
         | 
| 164 | 
            +
              components |
         | 
| 165 | 
            +
              .*-args
         | 
| 166 | 
            +
            )$
         | 
| 167 | 
            +
                """,
         | 
| 168 | 
            +
                    re.VERBOSE,
         | 
| 169 | 
            +
                )
         | 
| 170 | 
            +
             | 
| 171 | 
            +
                def _remove_implementation_details_from_model(self, model):
         | 
| 172 | 
            +
                    if isinstance(model, dict):
         | 
| 173 | 
            +
                        return self._remove_implementation_details_from_model_dict(model)
         | 
| 174 | 
            +
                    elif isinstance(model, list):
         | 
| 175 | 
            +
                        return self._remove_implementation_details_from_model_list(model)
         | 
| 176 | 
            +
                    else:
         | 
| 177 | 
            +
                        return model
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                def _remove_implementation_details_from_model_dict(self, model):
         | 
| 180 | 
            +
                    ret = {}
         | 
| 181 | 
            +
                    for k, v in model.items():
         | 
| 182 | 
            +
                        if not self.IMPLEMENTATION_DETAILS.match(k):
         | 
| 183 | 
            +
                            new_value = self._remove_implementation_details_from_model(v)
         | 
| 184 | 
            +
                            if new_value is not None and new_value != {} and new_value != []:
         | 
| 185 | 
            +
                                ret[k] = new_value
         | 
| 186 | 
            +
                    return ret
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                def _remove_implementation_details_from_model_list(self, model):
         | 
| 189 | 
            +
                    ret = []
         | 
| 190 | 
            +
                    for v in model:
         | 
| 191 | 
            +
                        new_value = self._remove_implementation_details_from_model(v)
         | 
| 192 | 
            +
                        if new_value is not None and new_value != {} and new_value != []:
         | 
| 193 | 
            +
                            ret.append(new_value)
         | 
| 194 | 
            +
                    return ret
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                def call_components(self, method_name, **kwargs):
         | 
| 197 | 
            +
                    for component in self.config.components:
         | 
| 198 | 
            +
                        if hasattr(component, method_name):
         | 
| 199 | 
            +
                            getattr(component, method_name)(**kwargs)
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                def _get_model_features(self, model):
         | 
| 202 | 
            +
                    features = []
         | 
| 203 | 
            +
                    if model.get("requests", {}):
         | 
| 204 | 
            +
                        features.append("requests")
         | 
| 205 | 
            +
                    if model.get("draft", {}):
         | 
| 206 | 
            +
                        features.append("drafts")
         | 
| 207 | 
            +
                    if model.get("files", {}):
         | 
| 208 | 
            +
                        features.append("files")
         | 
| 209 | 
            +
                    return features
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                def _get_model_api_endpoint(self, model):
         | 
| 212 | 
            +
                    try:
         | 
| 213 | 
            +
                        alias = model["api-blueprint"]["alias"]
         | 
| 214 | 
            +
                        return api_url_for(f"{alias}.search", _external=True)
         | 
| 215 | 
            +
                    except:  # NOSONAR noqa
         | 
| 216 | 
            +
                        logger.exception("Failed to get model api endpoint")
         | 
| 217 | 
            +
                        return None
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                def _get_model_draft_endpoint(self, model):
         | 
| 220 | 
            +
                    try:
         | 
| 221 | 
            +
                        alias = model["api-blueprint"]["alias"]
         | 
| 222 | 
            +
                        return api_url_for(f"{alias}.search_user_records", _external=True)
         | 
| 223 | 
            +
                    except:  # NOSONAR noqa
         | 
| 224 | 
            +
                        logger.exception("Failed to get model draft endpoint")
         | 
| 225 | 
            +
                        return None
         | 
| 226 | 
            +
             | 
| 227 | 
            +
                def _get_model_html_endpoint(self, model):
         | 
| 228 | 
            +
                    try:
         | 
| 229 | 
            +
                        return urljoin(
         | 
| 230 | 
            +
                            self._get_model_api_endpoint(model),
         | 
| 231 | 
            +
                            model["resource-config"]["base-html-url"],
         | 
| 232 | 
            +
                        )
         | 
| 233 | 
            +
                    except:  # NOSONAR noqa
         | 
| 234 | 
            +
                        logger.exception("Failed to get model html endpoint")
         | 
| 235 | 
            +
                        return None
         | 
| 236 | 
            +
             | 
| 237 | 
            +
                def _get_model_schema_endpoint(self, model):
         | 
| 238 | 
            +
                    try:
         | 
| 239 | 
            +
                        return url_for(
         | 
| 240 | 
            +
                            "oarepo_runtime_info.schema",
         | 
| 241 | 
            +
                            schema=model["json-schema-settings"]["name"],
         | 
| 242 | 
            +
                            _external=True,
         | 
| 243 | 
            +
                        )
         | 
| 244 | 
            +
                    except:  # NOSONAR noqa
         | 
| 245 | 
            +
                        logger.exception("Failed to get model schema endpoint")
         | 
| 246 | 
            +
                        return None
         | 
| 247 | 
            +
             | 
| 248 | 
            +
                def _get_model_model_endpoint(self, model):
         | 
| 249 | 
            +
                    try:
         | 
| 250 | 
            +
                        return url_for("oarepo_runtime_info.model", model=model, _external=True)
         | 
| 251 | 
            +
                    except:  # NOSONAR noqa
         | 
| 252 | 
            +
                        logger.exception("Failed to get model model endpoint")
         | 
| 253 | 
            +
                        return None
         | 
| 254 | 
            +
             | 
| 255 | 
            +
                def _get_model_schemas(self, service):
         | 
| 256 | 
            +
                    try:
         | 
| 257 | 
            +
                        record_cls = service.config.record_cls
         | 
| 258 | 
            +
                        schema = getattr(record_cls, "schema", None)
         | 
| 259 | 
            +
                        if schema is not None:
         | 
| 260 | 
            +
                            return [schema.value]
         | 
| 261 | 
            +
                    except:  # NOSONAR noqa
         | 
| 262 | 
            +
                        logger.exception("Failed to get model schemas")
         | 
| 263 | 
            +
                    return []
         | 
| 264 | 
            +
             | 
| 265 | 
            +
                def _get_service(self, model_data):
         | 
| 266 | 
            +
                    service_id = model_data["service-config"]["service-id"]
         | 
| 267 | 
            +
                    try:
         | 
| 268 | 
            +
                        service = current_service_registry.get(service_id)
         | 
| 269 | 
            +
                    except KeyError:
         | 
| 270 | 
            +
                        return None
         | 
| 271 | 
            +
                    return service
         | 
| 272 | 
            +
             | 
| 273 | 
            +
             | 
| 274 | 
            +
            def create_wellknown_blueprint(app):
         | 
| 275 | 
            +
                """Create blueprint."""
         | 
| 276 | 
            +
                config_class = obj_or_import_string(
         | 
| 277 | 
            +
                    app.config.get("INFO_ENDPOINT_CONFIG", InfoConfig)
         | 
| 278 | 
            +
                )
         | 
| 279 | 
            +
                return InfoResource(config=config_class(app)).as_blueprint()
         | 
| 280 | 
            +
             | 
| 281 | 
            +
             | 
| 282 | 
            +
            def get_package_version(package_name):
         | 
| 283 | 
            +
                """Get package version."""
         | 
| 284 | 
            +
                from pkg_resources import get_distribution
         | 
| 285 | 
            +
             | 
| 286 | 
            +
                try:
         | 
| 287 | 
            +
                    return re.sub(r"\+.*", "", get_distribution(package_name).version)
         | 
| 288 | 
            +
                except Exception:  # NOSONAR noqa
         | 
| 289 | 
            +
                    logger.exception(f"Failed to get package version for {package_name}")
         | 
| 290 | 
            +
                    return None
         | 
| 291 | 
            +
             | 
| 292 | 
            +
             | 
| 293 | 
            +
            def api_url_for(endpoint, _external=True, **values):
         | 
| 294 | 
            +
                """API url_for."""
         | 
| 295 | 
            +
                site_api_url = current_app.config["SITE_API_URL"]
         | 
| 296 | 
            +
                site_url = current_app.config["SITE_UI_URL"]
         | 
| 297 | 
            +
                base_url = url_for(endpoint, **values, _external=_external)
         | 
| 298 | 
            +
                if base_url.startswith(site_api_url):
         | 
| 299 | 
            +
                    return base_url
         | 
| 300 | 
            +
                if base_url.startswith(site_url):
         | 
| 301 | 
            +
                    return base_url.replace(site_url, site_api_url)
         | 
| 302 | 
            +
                raise ValueError(f"URL {base_url} does not start with {site_url} or {site_api_url}")
         | 
| @@ -0,0 +1,79 @@ | |
| 1 | 
            +
            from invenio_records.systemfields import SystemField
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            from .mapping import MappingSystemFieldMixin
         | 
| 4 | 
            +
             | 
| 5 | 
            +
             | 
| 6 | 
            +
            class SyntheticSystemField(MappingSystemFieldMixin, SystemField):
         | 
| 7 | 
            +
                """
         | 
| 8 | 
            +
                    A class that provides a synthetic system field, that is a system field that
         | 
| 9 | 
            +
                    generates its content from what is already present inside the record.
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    The field is not stored in the record, but is generated on the fly when
         | 
| 12 | 
            +
                    the record is being indexed.
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    Usage:
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    1. Create a new class that inherits from SyntheticSystemField
         | 
| 17 | 
            +
                    2. Implement the _value method that returns the value of the field from a data (
         | 
| 18 | 
            +
                       either a dictionary or an instance of the record class)
         | 
| 19 | 
            +
                    3. Put the class onto the record. If you use oarepo-model-builder, add it to the model
         | 
| 20 | 
            +
                       like:
         | 
| 21 | 
            +
                       ```yaml
         | 
| 22 | 
            +
                record:
         | 
| 23 | 
            +
                  record:
         | 
| 24 | 
            +
                    extra-code: |-2
         | 
| 25 | 
            +
                          # extra custom fields for facets
         | 
| 26 | 
            +
                          faculty = {{common.theses.synthetic_fields.FacultySystemField}}()
         | 
| 27 | 
            +
                          department = {{common.theses.synthetic_fields.DepartmentSystemField}}()
         | 
| 28 | 
            +
                          defenseYear = {{common.theses.synthetic_fields.DefenseYearSystemField}}()
         | 
| 29 | 
            +
                       ```
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    4. Add the extra fields to the mapping and facets. If using oarepo-model-builder, add it to the
         | 
| 32 | 
            +
                       model like the following piece of code and compile the model:
         | 
| 33 | 
            +
                       ```yaml
         | 
| 34 | 
            +
                record:
         | 
| 35 | 
            +
                  properties:
         | 
| 36 | 
            +
                    faculty:
         | 
| 37 | 
            +
                      type: vocabulary
         | 
| 38 | 
            +
                      vocabulary-type: institutions
         | 
| 39 | 
            +
                      facets:
         | 
| 40 | 
            +
                        facet-groups:
         | 
| 41 | 
            +
                        - default
         | 
| 42 | 
            +
                      label.cs: Fakulta
         | 
| 43 | 
            +
                      label.en: Faculty
         | 
| 44 | 
            +
             | 
| 45 | 
            +
             | 
| 46 | 
            +
                    department:
         | 
| 47 | 
            +
                      type: vocabulary
         | 
| 48 | 
            +
                      vocabulary-type: institutions
         | 
| 49 | 
            +
                      facets:
         | 
| 50 | 
            +
                        facet-groups:
         | 
| 51 | 
            +
                        - default
         | 
| 52 | 
            +
                      label.cs: Ústav
         | 
| 53 | 
            +
                      label.en: Department
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                    defenseYear:
         | 
| 56 | 
            +
                      type: integer
         | 
| 57 | 
            +
                      facets:
         | 
| 58 | 
            +
                        facet-groups:
         | 
| 59 | 
            +
                        - default
         | 
| 60 | 
            +
                      label.cs: Rok obhajoby
         | 
| 61 | 
            +
                      label.en: Defense year
         | 
| 62 | 
            +
                       ```
         | 
| 63 | 
            +
                """
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def search_dump(self, data):
         | 
| 66 | 
            +
                    dt = self._value(data)
         | 
| 67 | 
            +
                    if dt:
         | 
| 68 | 
            +
                        data[self.key] = dt
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                def search_load(self, data):
         | 
| 71 | 
            +
                    data.pop(self.key)
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def __get__(self, record, owner=None):
         | 
| 74 | 
            +
                    if record is None:
         | 
| 75 | 
            +
                        return self
         | 
| 76 | 
            +
                    return self._value(record)
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def _value(self, data):
         | 
| 79 | 
            +
                    raise NotImplementedError("You must implement the _value method")
         | 
| @@ -80,6 +80,7 @@ def prepare_cf_index(config: RecordServiceConfig): | |
| 80 80 | 
             
                    # get mapping
         | 
| 81 81 | 
             
                    mapping = fld.mapping
         | 
| 82 82 | 
             
                    settings = fld.mapping_settings
         | 
| 83 | 
            +
                    dynamic_templates = fld.dynamic_templates
         | 
| 83 84 |  | 
| 84 85 | 
             
                    # upload mapping
         | 
| 85 86 | 
             
                    try:
         | 
| @@ -98,20 +99,25 @@ def prepare_cf_index(config: RecordServiceConfig): | |
| 98 99 | 
             
                                ),
         | 
| 99 100 | 
             
                                using=current_search_client,
         | 
| 100 101 | 
             
                            )
         | 
| 101 | 
            -
                            update_index(draft_index, settings, mapping)
         | 
| 102 | 
            +
                            update_index(draft_index, settings, mapping, dynamic_templates)
         | 
| 102 103 |  | 
| 103 104 | 
             
                    except search.RequestError as e:
         | 
| 104 105 | 
             
                        click.secho("An error occurred while creating custom fields.", fg="red")
         | 
| 105 106 | 
             
                        click.secho(e.info["error"]["reason"], fg="red")
         | 
| 106 107 |  | 
| 107 108 |  | 
| 108 | 
            -
            def update_index(record_index, settings, mapping):
         | 
| 109 | 
            +
            def update_index(record_index, settings, mapping, dynamic_templates=None):
         | 
| 109 110 | 
             
                if settings:
         | 
| 110 111 | 
             
                    record_index.close()
         | 
| 111 112 | 
             
                    record_index.put_settings(body=settings)
         | 
| 112 113 | 
             
                    record_index.open()
         | 
| 114 | 
            +
                body = {}
         | 
| 113 115 | 
             
                if mapping:
         | 
| 114 | 
            -
                     | 
| 116 | 
            +
                    body["properties"] = mapping
         | 
| 117 | 
            +
                if dynamic_templates:
         | 
| 118 | 
            +
                    body["dynamic_templates"] = dynamic_templates
         | 
| 119 | 
            +
                if body:
         | 
| 120 | 
            +
                    record_index.put_mapping(body=body)
         | 
| 115 121 |  | 
| 116 122 |  | 
| 117 123 | 
             
            def get_mapping_fields(record_class) -> Iterable[MappingSystemFieldMixin]:
         | 
| @@ -1,4 +1,3 @@ | |
| 1 | 
            -
            import inspect
         | 
| 2 1 |  | 
| 3 2 | 
             
            from invenio_records_resources.services.records.results import (
         | 
| 4 3 | 
             
                RecordItem as BaseRecordItem,
         | 
| @@ -7,9 +6,12 @@ from invenio_records_resources.services.records.results import ( | |
| 7 6 | 
             
                RecordList as BaseRecordList,
         | 
| 8 7 | 
             
            )
         | 
| 9 8 |  | 
| 9 | 
            +
             | 
| 10 10 | 
             
            class RecordItem(BaseRecordItem):
         | 
| 11 11 | 
             
                """Single record result."""
         | 
| 12 | 
            +
             | 
| 12 13 | 
             
                components = []
         | 
| 14 | 
            +
             | 
| 13 15 | 
             
                @property
         | 
| 14 16 | 
             
                def data(self):
         | 
| 15 17 | 
             
                    if self._data:
         | 
| @@ -19,8 +21,10 @@ class RecordItem(BaseRecordItem): | |
| 19 21 | 
             
                        c.update_data(self._identity, self._record, _data)
         | 
| 20 22 | 
             
                    return _data
         | 
| 21 23 |  | 
| 24 | 
            +
             | 
| 22 25 | 
             
            class RecordList(BaseRecordList):
         | 
| 23 26 | 
             
                components = []
         | 
| 27 | 
            +
             | 
| 24 28 | 
             
                @property
         | 
| 25 29 | 
             
                def hits(self):
         | 
| 26 30 | 
             
                    """Iterator over the hits."""
         | 
| @@ -0,0 +1,8 @@ | |
| 1 | 
            +
            def try_sequence(*funcs, ignored_exceptions=(), raised_exception=Exception):
         | 
| 2 | 
            +
                raised_exceptions = []
         | 
| 3 | 
            +
                for func in funcs:
         | 
| 4 | 
            +
                    try:
         | 
| 5 | 
            +
                        return func()
         | 
| 6 | 
            +
                    except ignored_exceptions as e:
         | 
| 7 | 
            +
                        raised_exceptions.append(e)
         | 
| 8 | 
            +
                raise raised_exception(raised_exceptions) from raised_exceptions[-1]
         | 
| @@ -1,5 +1,5 @@ | |
| 1 1 | 
             
            oarepo_runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 2 | 
            -
            oarepo_runtime/ext.py,sha256= | 
| 2 | 
            +
            oarepo_runtime/ext.py,sha256=XPEEg1fDDAC6uihCM3ASbKmkgdL050lg9qTdrqqbI4A,1959
         | 
| 3 3 | 
             
            oarepo_runtime/ext_config.py,sha256=c9q8zc78bjBosv1lbHGfenX2S6L-_9v0RZeQjbczvdI,1941
         | 
| 4 4 | 
             
            oarepo_runtime/profile.py,sha256=QzrQoZncjoN74ZZnpkEKakNk08KCzBU7m6y42RN8AMY,1637
         | 
| 5 5 | 
             
            oarepo_runtime/proxies.py,sha256=IuwZDwEqRcKxdqNdMrlSzOSiYjtWyJIgvg4zbFTWI8I,207
         | 
| @@ -10,7 +10,7 @@ oarepo_runtime/cli/base.py,sha256=94RBTa8TOSPxEyEUmYLGXaWen-XktP2-MIbTtZSlCZo,54 | |
| 10 10 | 
             
            oarepo_runtime/cli/cf.py,sha256=W0JEJK2JqKubQw8qtZJxohmADDRUBode4JZAqYLDGvc,339
         | 
| 11 11 | 
             
            oarepo_runtime/cli/check.py,sha256=AvC5VHAnwmtCd8R-Caj8v6nCAREKjObTdNtLJ24aJO8,4935
         | 
| 12 12 | 
             
            oarepo_runtime/cli/configuration.py,sha256=cLXoGDtjuA5uv9ZfYFcH0C4wcadj0qWC3P_E4Bf5-z0,1061
         | 
| 13 | 
            -
            oarepo_runtime/cli/fixtures.py,sha256 | 
| 13 | 
            +
            oarepo_runtime/cli/fixtures.py,sha256=-OVPurbaIsyZ2MK7LL74q_bXHkI_YTScUcTanT2D1hY,2760
         | 
| 14 14 | 
             
            oarepo_runtime/cli/index.py,sha256=VB7g-HSCd-lR6rk0GLn9i-Rt9JlEnEiQrGnAQlklFME,5447
         | 
| 15 15 | 
             
            oarepo_runtime/cli/validate.py,sha256=HpSvHQCGHlrdgdpKix9cIlzlBoJEiT1vACZdMnOUGEY,2827
         | 
| 16 16 | 
             
            oarepo_runtime/datastreams/__init__.py,sha256=_i52Ek9J8DMARST0ejZAZPzUKm55xrrlKlCSO7dl6y4,1008
         | 
| @@ -19,7 +19,7 @@ oarepo_runtime/datastreams/catalogue.py,sha256=D6leq-FPT3RP3SniEAXPm66v3q8ZdQnaU | |
| 19 19 | 
             
            oarepo_runtime/datastreams/datastreams.py,sha256=wnMk1UFv-cWXRO0jHwRNoJBO0cbZaHqrLnH7vgfnf78,4485
         | 
| 20 20 | 
             
            oarepo_runtime/datastreams/errors.py,sha256=WyZLU53EdFJTLv6K2ooM_M6ISjLS-U1dDw6B7guOLSc,1540
         | 
| 21 21 | 
             
            oarepo_runtime/datastreams/ext.py,sha256=ivugdVMCqwugK-5SeX14a-dMq6VaTt7DM2wFU357tR4,1406
         | 
| 22 | 
            -
            oarepo_runtime/datastreams/fixtures.py,sha256= | 
| 22 | 
            +
            oarepo_runtime/datastreams/fixtures.py,sha256=7uqA7YYtP4fs5Ktu5UYdbvZP4xm8cKiqykPvUyS6JDE,7770
         | 
| 23 23 | 
             
            oarepo_runtime/datastreams/json.py,sha256=OAiaH93eqpH5qNQSPKKc8K-hXKAn5lB0PUKwwZFqJSw,153
         | 
| 24 24 | 
             
            oarepo_runtime/datastreams/semi_asynchronous.py,sha256=TJWby2MDKXm5feRocoWB-8OhsShq5R9HoZ74O1rGBOk,2934
         | 
| 25 25 | 
             
            oarepo_runtime/datastreams/synchronous.py,sha256=t5lfnMkLqy3jK5zMl-nIuA0HlMPiHGjwCqZ8XQP-3GM,2595
         | 
| @@ -40,6 +40,8 @@ oarepo_runtime/datastreams/writers/utils.py,sha256=Lk_ZLNeXTLuFEn04lw1-6bJ7duG6k | |
| 40 40 | 
             
            oarepo_runtime/datastreams/writers/validation_errors.py,sha256=wOCXdniR6so_4ExpdFYYgBRyENp7_6kVFZM2L-Hy3G8,661
         | 
| 41 41 | 
             
            oarepo_runtime/datastreams/writers/yaml.py,sha256=XchUJHQ58E2Mfgs8elImXbL38jFtI8Hfoye6yaR0gKI,1482
         | 
| 42 42 | 
             
            oarepo_runtime/i18n/__init__.py,sha256=G4PJ_kQlPDiBW6ntjQZ-O4qHQgkJWAXsNLUuOBcglNM,402
         | 
| 43 | 
            +
            oarepo_runtime/info/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 44 | 
            +
            oarepo_runtime/info/views.py,sha256=W1aa6c6ZYABcjSgH3DfjadHrFHqn2Myc1XarkEvWxU0,10399
         | 
| 43 45 | 
             
            oarepo_runtime/records/__init__.py,sha256=Bxm2XNtB9j6iOAKGSVsjXTKIZS-iXKPzakCqyNkEix8,432
         | 
| 44 46 | 
             
            oarepo_runtime/records/dumpers/__init__.py,sha256=OmzNhLdMNKibmCksnj9eTX9xPBG30dziiK3j3bAAp3k,233
         | 
| 45 47 | 
             
            oarepo_runtime/records/dumpers/edtf_interval.py,sha256=YCShZAoqBQYaxVilEVotS-jXZsxxoXO67yu2urhkaMA,1198
         | 
| @@ -55,19 +57,20 @@ oarepo_runtime/records/systemfields/__init__.py,sha256=JpDSVry8TWvUNGlR7AXd_D9ND | |
| 55 57 | 
             
            oarepo_runtime/records/systemfields/featured_file.py,sha256=4_SXyGNMxAANeYOGrT4QLJmzVdF4FFeTP_n4T9KZ3M8,1725
         | 
| 56 58 | 
             
            oarepo_runtime/records/systemfields/has_draftcheck.py,sha256=3XLRsZJWRpT4BFF1HTg6C27ECVmIcZ4RrdGzYC8S7v0,1518
         | 
| 57 59 | 
             
            oarepo_runtime/records/systemfields/icu.py,sha256=-vGPbVkEUS53dIm50pEcRlk1T6h002s7fBY4Ic2X75c,5951
         | 
| 58 | 
            -
            oarepo_runtime/records/systemfields/mapping.py,sha256= | 
| 60 | 
            +
            oarepo_runtime/records/systemfields/mapping.py,sha256=GJeQUwH-J2Gxtd9cpzGUT0bxirObXkwCZ33_Q6WZTK0,787
         | 
| 59 61 | 
             
            oarepo_runtime/records/systemfields/record_status.py,sha256=iXasHCIc-veaOHyiSpxHL8CWNpleh_BDBybkv79iqb0,945
         | 
| 62 | 
            +
            oarepo_runtime/records/systemfields/synthetic.py,sha256=kX_cSz5llbBXMjpK6MTjci1zah641eqV3ivXs5fZpcs,2448
         | 
| 60 63 | 
             
            oarepo_runtime/resources/__init__.py,sha256=v8BGrOTu_FjKzd0eozV7Q4GoGxyfybsL2cI-tbP5Pys,185
         | 
| 61 64 | 
             
            oarepo_runtime/resources/file_resource.py,sha256=Ta3bFce7l0xwqkkOMOEu9mxbB8BbKj5HUHRHmidhnl8,414
         | 
| 62 65 | 
             
            oarepo_runtime/resources/localized_ui_json_serializer.py,sha256=4Kle34k-_uu3Y9JJ2vAXcQ9DqYRxXgy-_iZhiFuukmE,1684
         | 
| 63 66 | 
             
            oarepo_runtime/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 64 | 
            -
            oarepo_runtime/services/results.py,sha256= | 
| 67 | 
            +
            oarepo_runtime/services/results.py,sha256=DsBPniwhNvOkYucS8Z0v3EC28LemY7JuZNVhl_17PhA,1500
         | 
| 65 68 | 
             
            oarepo_runtime/services/search.py,sha256=ywfwGH7oAM44WeOSjlIsY_qoCMZJ1TlTLd_NgE2ow3Y,5296
         | 
| 66 69 | 
             
            oarepo_runtime/services/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 67 70 | 
             
            oarepo_runtime/services/config/permissions_presets.py,sha256=zApeA-2DYAlD--SzVz3vq_OFjq48Ko0pe08e4o2vxr4,6114
         | 
| 68 71 | 
             
            oarepo_runtime/services/config/service.py,sha256=Uzo3rJGWljbkLcWd-E1BJOEglJHli8eUSIgFTJt9sXE,1566
         | 
| 69 72 | 
             
            oarepo_runtime/services/custom_fields/__init__.py,sha256=HVqgIIoPqg-pJHkP1KY_Phe-9fSD0RsnF02H5OS9wCM,2262
         | 
| 70 | 
            -
            oarepo_runtime/services/custom_fields/mappings.py,sha256= | 
| 73 | 
            +
            oarepo_runtime/services/custom_fields/mappings.py,sha256=1mb8nYeUlQxbBolsKURGKUIpIV1NDb-7Mcur32jjIjg,4433
         | 
| 71 74 | 
             
            oarepo_runtime/services/expansions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 72 75 | 
             
            oarepo_runtime/services/expansions/expandable_fields.py,sha256=7DWKFL6ml8J7zGI6wm9LO7Xd6R0LSylsuq4lyRumNHQ,745
         | 
| 73 76 | 
             
            oarepo_runtime/services/expansions/service.py,sha256=HaEy76XOhDf__sQ91hi-8iH1hthM9q07pRhOmyZyVrs,144
         | 
| @@ -102,11 +105,12 @@ oarepo_runtime/translations/cs/LC_MESSAGES/messages.po,sha256=vGZQo5NlTtj_qsJuDw | |
| 102 105 | 
             
            oarepo_runtime/translations/en/LC_MESSAGES/messages.mo,sha256=FKAl1wlg2NhtQ1-9U2dkUwcotR959j5GuUKJygCYpwI,445
         | 
| 103 106 | 
             
            oarepo_runtime/translations/en/LC_MESSAGES/messages.po,sha256=rZ2PGvkcJbmuwrWeFX5edk0zJIzZnL83M9HSAceDP_U,1193
         | 
| 104 107 | 
             
            oarepo_runtime/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 108 | 
            +
            oarepo_runtime/utils/functools.py,sha256=eueB-4MRv9wrOARs70ey21JvBb63gGeJV6m1fdGcnPQ,319
         | 
| 105 109 | 
             
            oarepo_runtime/utils/path.py,sha256=V1NVyk3m12_YLbj7QHYvUpE1wScO78bYsX1LOLeXDkI,3108
         | 
| 106 110 | 
             
            tests/pkg_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 107 | 
            -
            oarepo_runtime-1.5. | 
| 108 | 
            -
            oarepo_runtime-1.5. | 
| 109 | 
            -
            oarepo_runtime-1.5. | 
| 110 | 
            -
            oarepo_runtime-1.5. | 
| 111 | 
            -
            oarepo_runtime-1.5. | 
| 112 | 
            -
            oarepo_runtime-1.5. | 
| 111 | 
            +
            oarepo_runtime-1.5.7.dist-info/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
         | 
| 112 | 
            +
            oarepo_runtime-1.5.7.dist-info/METADATA,sha256=SHRsje7uchCfZ6fsSFhN26JCbGrwYexHiQI0DYNgVpg,4786
         | 
| 113 | 
            +
            oarepo_runtime-1.5.7.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
         | 
| 114 | 
            +
            oarepo_runtime-1.5.7.dist-info/entry_points.txt,sha256=QrlXAKuPDVBinaSh_v3yO9_Nb9ZNmJCJ0VFcCW-z0Jg,327
         | 
| 115 | 
            +
            oarepo_runtime-1.5.7.dist-info/top_level.txt,sha256=bHhlkT1_RQC4IkfTQCqA3iN4KCB6cSFQlsXpQMSP-bE,21
         | 
| 116 | 
            +
            oarepo_runtime-1.5.7.dist-info/RECORD,,
         | 
| @@ -4,5 +4,8 @@ oarepo_runtime = oarepo_runtime.ext:OARepoRuntime | |
| 4 4 | 
             
            [invenio_base.apps]
         | 
| 5 5 | 
             
            oarepo_runtime = oarepo_runtime.ext:OARepoRuntime
         | 
| 6 6 |  | 
| 7 | 
            +
            [invenio_base.blueprints]
         | 
| 8 | 
            +
            oarepo_runtime_info = oarepo_runtime.info.views:create_wellknown_blueprint
         | 
| 9 | 
            +
             | 
| 7 10 | 
             
            [invenio_celery.tasks]
         | 
| 8 11 | 
             
            oarepo_runtime_datastreams = oarepo_runtime.datastreams
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         |