Repository: ITISFoundation/osparc-simcore
Pull request #7508: 🎨 replace project_id and node_id with appropriate labels when exporting
Author: login: GitHK
Labels: name: a:storage
description: issue related to storage service
name: 🤖-automerge
description: marks PR as ready to be merged for Mergify
Created: 2025-04-10T12:50:11Z | Merged: 2025-04-22T12:26:33Z

<!-- Title Annotations:

  WIP: work in progress
  🐛    Fix a bug.
  ✨    Introduce new features.
  🎨    Enhance existing feature.
  ♻️    Refactor code.
  🚑️    Critical hotfix.
  ⚗️    Perform experiments.
  ⬆️    Upgrade dependencies.
  📝    Add or update documentation.
  🔨    Add or update development scripts.
  🔒️    Fix security issues.
  ⚠️    Changes in ops configuration etc. are required before deploying.
        [ Please add a link to the associated ops-issue or PR, such as in https://github.com/ITISFoundation/osparc-ops-environments or https://git.speag.com/oSparc/osparc-infra ]
  🗃️    Database table changed (relevant for devops).
  👽️    Public API changes (meaning: dev features are moved to being exposed in production)
  🚨    Do manual testing when deployed

or from https://gitmoji.dev/
-->

## What do these changes do?

<!-- Badge to openapi specs
[![ReDoc](https://img.shields.io/badge/OpenAPI-ReDoc-85ea2d?logo=openapiinitiative)](https://redocly.github.io/redoc/?url=HERE-URL-TO-RAW-FILE)
-->

When exporting data, `project_id` is replaced with the project's name while `node_id` is replaced with the node's label.
Also, special character `/` is escaped with `_`.

## Related issue/s

<!-- Link pull request to an issue
  SEE https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue

- resolves ITISFoundation/osparc-issues#428
- fixes #26
-->


## How to test

<!-- Give REVIEWERS some hits or code snippets on how could this be tested -->

## Dev-ops checklist

- [x] No ENV changes or I properly updated ENV ([read the instruction](https://git.speag.com/oSparc/osparc-ops-deployment-configuration/-/blob/configs/README.md?ref_type=heads#how-to-update-env-variables))

<!-- Some checks that might help your code run stable on production, and help devops assess criticality.
Modified from https://oschvr.com/posts/what-id-like-as-sre/

- How can DevOps check the health of the service ?
- How can DevOps safely and gracefully restart the service ?
- How and why would this code fail ?
- What kind of metrics are you exposing ?
- Is there any documentation/design specification for the service ?
- How (e.g. through which loglines) can DevOps detect unexpected situations that require escalation to human ?
- What are the resource limitations (CPU, RAM) expected for this service ?
- Are all relevant variables documented and adjustable via environment variables (i.e. no hardcoded magic numbers) ?
-->


Linked issue #26: Pipeline creation
**User story**

As a **developer** I want to be able to create arbitrary pipelines out of computational services in the frontend. Mockup services can be used for finding compatible services but the metadata for input/output data structures need to be defined such that they can be used for the MVP.

**Definition of Done**

- [x] Services can interactively added to the pipeline
- [x] Only compatible services can be attached to each other
- [x] Define data structure needed for this (compatible with the pipeline that is being use in the comp. backend)
- [x] Multiple in- and outputs can exist (ports)
- [x] Settings can be defined by input
- [x] Branching
Linked issue comments: user: login: drniiken
created_at: 2018-05-02T17:30:43Z
body: * datastandards
* jason.ad
* decided for
author_association: MEMBER
user: login: sanderegg
created_at: 2018-06-06T11:49:33Z
body: all settings are inputs, with a default
- [ ] the link shall be deletable
- [ ] the settings is read-only when a node is connected
- [ ] all settings visible in the workbench view
author_association: MEMBER

Diff:
diff --git a/services/storage/src/simcore_service_storage/simcore_s3_dsm.py b/services/storage/src/simcore_service_storage/simcore_s3_dsm.py
index 616571176371..5187e2345511 100644
--- a/services/storage/src/simcore_service_storage/simcore_s3_dsm.py
+++ b/services/storage/src/simcore_service_storage/simcore_s3_dsm.py
@@ -1312,6 +1312,7 @@ async def create_s3_export(
 
             await create_and_upload_export(
                 get_s3_client(self.app),
+                ProjectRepository.instance(get_db_engine(self.app)),
                 self.simcore_bucket_name,
                 source_object_keys=source_object_keys,
                 destination_object_keys=destination_object_key,
diff --git a/services/storage/src/simcore_service_storage/utils/simcore_s3_dsm_utils.py b/services/storage/src/simcore_service_storage/utils/simcore_s3_dsm_utils.py
index 5764151fdf12..d9e36c9f8567 100644
--- a/services/storage/src/simcore_service_storage/utils/simcore_s3_dsm_utils.py
+++ b/services/storage/src/simcore_service_storage/utils/simcore_s3_dsm_utils.py
@@ -8,8 +8,9 @@
 from aws_library.s3._constants import STREAM_READER_CHUNK_SIZE
 from aws_library.s3._models import S3ObjectKey
 from models_library.api_schemas_storage.storage_schemas import S3BucketName
-from models_library.projects import ProjectID
+from models_library.projects import ProjectID, ProjectIDStr
 from models_library.projects_nodes_io import (
+    NodeIDStr,
     SimcoreS3DirectoryID,
     SimcoreS3FileID,
     StorageFileID,
@@ -27,6 +28,7 @@
 from ..models import FileMetaData, FileMetaDataAtDB, GenericCursor, PathMetaData
 from ..modules.db.access_layer import AccessLayerRepository
 from ..modules.db.file_meta_data import FileMetaDataRepository, TotalChildren
+from ..modules.db.projects import ProjectRepository
 from .utils import convert_db_to_model
 
 
@@ -165,17 +167,55 @@ def _base_path_parent(base_path: UserSelectionStr, s3_object: S3ObjectKey) -> st
     return f"{result}"
 
 
+def _get_project_ids(user_selecton: set[UserSelectionStr]) -> list[ProjectID]:
+    results = []
+    for selected in user_selecton:
+        project_id = ProjectID(Path(selected).parts[0])
+        results.append(project_id)
+    return results
+
+
+def _replace_node_id_project_id_in_path(
+    ids_names_map: dict[ProjectID, dict[ProjectIDStr | NodeIDStr, str]], path: str
+) -> str:
+    path_parts = Path(path).parts
+    if len(path_parts) == 0:
+        return path
+
+    if len(path_parts) == 1:
+        return ids_names_map[ProjectID(path)][path].replace("/", "_")
+
+    project_id_str = path_parts[0]
+    project_id = ProjectID(project_id_str)
+    node_id_str = path_parts[1]
+    return "/".join(
+        (
+            ids_names_map[project_id][project_id_str].replace("/", "_"),
+            ids_names_map[project_id][node_id_str].replace("/", "_"),
+            *path_parts[2:],
+        )
+    )
+
+
 async def create_and_upload_export(
     s3_client: SimcoreS3API,
+    project_repository: ProjectRepository,
     bucket: S3BucketName,
     *,
     source_object_keys: set[tuple[UserSelectionStr, StorageFileID]],
     destination_object_keys: StorageFileID,
     progress_bar: ProgressBarData,
 ) -> None:
+    ids_names_map = await project_repository.get_project_id_and_node_id_to_names_map(
+        project_uuids=_get_project_ids(user_selecton={x[0] for x in source_object_keys})
+    )
+
     archive_entries: ArchiveEntries = [
         (
-            _base_path_parent(selection, s3_object),
+            _base_path_parent(
+                _replace_node_id_project_id_in_path(ids_names_map, selection),
+                _replace_node_id_project_id_in_path(ids_names_map, s3_object),
+            ),
             await s3_client.get_bytes_streamer_from_object(bucket, s3_object),
         )
         for (selection, s3_object) in source_object_keys
diff --git a/services/storage/tests/unit/test_simcore_s3_dsm_utils.py b/services/storage/tests/unit/test_simcore_s3_dsm_utils.py
index 56f7d9bd92af..e87d2a5e0dbd 100644
--- a/services/storage/tests/unit/test_simcore_s3_dsm_utils.py
+++ b/services/storage/tests/unit/test_simcore_s3_dsm_utils.py
@@ -1,10 +1,16 @@
 from pathlib import Path
+from typing import Final
+from uuid import UUID
 
 import pytest
 from aws_library.s3._models import S3ObjectKey
+from models_library.projects import ProjectID, ProjectIDStr
+from models_library.projects_nodes_io import NodeIDStr
+from simcore_service_storage.models import NodeID
 from simcore_service_storage.utils.simcore_s3_dsm_utils import (
     UserSelectionStr,
     _base_path_parent,
+    _replace_node_id_project_id_in_path,
     compute_file_id_prefix,
     ensure_user_selection_from_same_base_directory,
 )
@@ -73,3 +79,53 @@ def test_ensure_user_selection_from_same_base_directory(
         ensure_user_selection_from_same_base_directory([f"{x}" for x in user_selection])
         == expected
     )
+
+
+_PID1: Final[ProjectID] = UUID(int=1)
+_PID2: Final[ProjectID] = UUID(int=2)
+_NID1: Final[NodeID] = UUID(int=3)
+_NID2: Final[NodeID] = UUID(int=4)
+_IDS_NAMES_MAP: Final[dict[ProjectID, dict[ProjectIDStr | NodeIDStr, str]]] = {
+    _PID1: {
+        f"{_PID1}": "project one",
+        f"{_NID1}": "project one -> node one",
+        f"{_NID2}": "project one -> node two",
+    },
+    _PID2: {
+        f"{_PID2}": "/project/two/",
+        f"{_NID1}": "/project/two/->/node/one/",
+        f"{_NID2}": "/project/two/->/node/two/",
+    },
+}
+
+
+@pytest.mark.parametrize(
+    "path, expected",
+    [
+        ("", ""),
+        (f"{_PID1}", "project one"),
+        (f"{_PID1}/{_NID1}", "project one/project one -> node one"),
+        (f"{_PID1}/{_NID1}/something", "project one/project one -> node one/something"),
+        (f"{_PID1}/{_NID1}/{_NID2}", f"project one/project one -> node one/{_NID2}"),
+        (
+            f"{_PID1}/{_NID1}/{_NID2}/something",
+            f"project one/project one -> node one/{_NID2}/something",
+        ),
+        (f"{_PID2}", "_project_two_"),
+        (f"{_PID2}/{_NID1}", "_project_two_/_project_two_->_node_one_"),
+        (
+            f"{_PID2}/{_NID1}/something",
+            "_project_two_/_project_two_->_node_one_/something",
+        ),
+        (
+            f"{_PID2}/{_NID1}/{_NID2}",
+            f"_project_two_/_project_two_->_node_one_/{_NID2}",
+        ),
+        (
+            f"{_PID2}/{_NID1}/{_NID2}/something",
+            f"_project_two_/_project_two_->_node_one_/{_NID2}/something",
+        ),
+    ],
+)
+def test__replace_node_id_project_id_in_path(path: str, expected: str):
+    assert _replace_node_id_project_id_in_path(_IDS_NAMES_MAP, path) == expected


Reviews:
user: login: giancarloromeo
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T13:35:26Z
user: login: sanderegg
body: just wondering why you did not re-use the function that is already around?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T15:36:09Z
user: login: pcrespov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T17:12:42Z
user: login: GitHK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T07:09:18Z
user: login: sanderegg
body: as discussed. thanks!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-11T10:17:46Z
user: login: matusdrobuliak66
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-17T06:48:21Z

Review comments:
diff_hunk: @@ -165,17 +170,57 @@ def _base_path_parent(base_path: UserSelectionStr, s3_object: S3ObjectKey) -> st
     return f"{result}"
 
 
+def _get_project_ids(user_selecton: set[UserSelectionStr]) -> list[ProjectID]:
+    results = []
+    for selected in user_selecton:
+        project_id = ProjectID(Path(selected).parts[0])
+        results.append(project_id)
+    return results
+
+
+def _replace_node_id_project_id_in_path(
+    ids_names_map: dict[ProjectID, dict[ProjectIDStr | NodeIDStr, str]], path: str
+) -> str:
+    path_parts = Path(path).parts
+    if len(path_parts) == 0:
+        return path
+
+    if len(path_parts) == 1:
+        return ids_names_map[ProjectID(path)][path].replace("/", "_")
+
+    project_id_str = path_parts[0]
+    project_id = ProjectID(project_id_str)
+    node_id_str = path_parts[1]
+    return "/".join(
+        (
+            ids_names_map[project_id][project_id_str].replace("/", "_"),
+            ids_names_map[project_id][node_id_str].replace("/", "_"),
+            *path_parts[2:],
+        )
+    )
+
+
 async def create_and_upload_export(
-    s3_client: SimcoreS3API,
+    app: FastAPI,
     bucket: S3BucketName,
     *,
     source_object_keys: set[tuple[UserSelectionStr, StorageFileID]],
     destination_object_keys: StorageFileID,
     progress_bar: ProgressBarData,
 ) -> None:
+    s3_client = get_s3_client(app)
+    ids_names_map = await ProjectRepository.instance(
+        get_db_engine(app)
+    ).get_project_id_and_node_id_to_names_map(
+        project_uuids=_get_project_ids(user_selecton={x[0] for x in source_object_keys})
+    )
+
     archive_entries: ArchiveEntries = [
path: services/storage/src/simcore_service_storage/utils/simcore_s3_dsm_utils.py
user: login: sanderegg
body: what about re-using this??
```python
def update_display_fields(self, id_name_mapping: dict[str, str]) -> None:
        display_path = f"{self.path}"
        for old, new in id_name_mapping.items():
            display_path = display_path.replace(old, urllib.parse.quote(new, safe=""))
        self.display_path = Path(display_path)
```
instead of `_replace_node_id_project_id_in_path` ?
created_at: 2025-04-10T15:35:07Z
author_association: MEMBER
diff_hunk: @@ -165,17 +170,57 @@ def _base_path_parent(base_path: UserSelectionStr, s3_object: S3ObjectKey) -> st
     return f"{result}"
 
 
+def _get_project_ids(user_selecton: set[UserSelectionStr]) -> list[ProjectID]:
+    results = []
+    for selected in user_selecton:
+        project_id = ProjectID(Path(selected).parts[0])
+        results.append(project_id)
+    return results
+
+
+def _replace_node_id_project_id_in_path(
+    ids_names_map: dict[ProjectID, dict[ProjectIDStr | NodeIDStr, str]], path: str
+) -> str:
+    path_parts = Path(path).parts
+    if len(path_parts) == 0:
+        return path
+
+    if len(path_parts) == 1:
+        return ids_names_map[ProjectID(path)][path].replace("/", "_")
+
+    project_id_str = path_parts[0]
+    project_id = ProjectID(project_id_str)
+    node_id_str = path_parts[1]
+    return "/".join(
+        (
+            ids_names_map[project_id][project_id_str].replace("/", "_"),
+            ids_names_map[project_id][node_id_str].replace("/", "_"),
+            *path_parts[2:],
+        )
+    )
+
+
 async def create_and_upload_export(
-    s3_client: SimcoreS3API,
+    app: FastAPI,
path: services/storage/src/simcore_service_storage/utils/simcore_s3_dsm_utils.py
user: login: pcrespov
body: IMO it is cleaner to constraint the inputs since you know what the function needs and avoid mistakenly coupling under the hood more modules. I suggest that you rather do somethin glike

```python
async def create_and_upload_export( s3_client: SimcoreS3API,  project_repo: ProjectRepository, ...)
   ...

```
and then provide them from the caller
```python

await  create_and_upload_export(get_s3_client(app), await ProjectRepository.instance(
        get_db_engine(app)
    ) , ...)

```
created_at: 2025-04-10T16:43:55Z
author_association: MEMBER
diff_hunk: @@ -165,17 +170,57 @@ def _base_path_parent(base_path: UserSelectionStr, s3_object: S3ObjectKey) -> st
     return f"{result}"
 
 
+def _get_project_ids(user_selecton: set[UserSelectionStr]) -> list[ProjectID]:
+    results = []
+    for selected in user_selecton:
+        project_id = ProjectID(Path(selected).parts[0])
+        results.append(project_id)
+    return results
+
+
+def _replace_node_id_project_id_in_path(
+    ids_names_map: dict[ProjectID, dict[ProjectIDStr | NodeIDStr, str]], path: str
+) -> str:
+    path_parts = Path(path).parts
+    if len(path_parts) == 0:
+        return path
+
+    if len(path_parts) == 1:
+        return ids_names_map[ProjectID(path)][path].replace("/", "_")
+
+    project_id_str = path_parts[0]
+    project_id = ProjectID(project_id_str)
+    node_id_str = path_parts[1]
+    return "/".join(
+        (
+            ids_names_map[project_id][project_id_str].replace("/", "_"),
+            ids_names_map[project_id][node_id_str].replace("/", "_"),
+            *path_parts[2:],
+        )
+    )
+
+
 async def create_and_upload_export(
-    s3_client: SimcoreS3API,
+    app: FastAPI,
     bucket: S3BucketName,
     *,
     source_object_keys: set[tuple[UserSelectionStr, StorageFileID]],
     destination_object_keys: StorageFileID,
     progress_bar: ProgressBarData,
 ) -> None:
+    s3_client = get_s3_client(app)
+    ids_names_map = await ProjectRepository.instance(
+        get_db_engine(app)
+    ).get_project_id_and_node_id_to_names_map(
+        project_uuids=_get_project_ids(user_selecton={x[0] for x in source_object_keys})
+    )
+
     archive_entries: ArchiveEntries = [
path: services/storage/src/simcore_service_storage/utils/simcore_s3_dsm_utils.py
user: login: GitHK
body: What did not convince me about this is:
- I require different escaping here
- does not run in linear time

What I like about it:
- only replaces what it finds (no risk of not finding anything)

The actual think that annoys me is the non liner runtime. If a user exports one or more projects, the more nodes there are the worst this gets.
This will scale with the number of files times the number of nodes + projects instead of just the number of files
created_at: 2025-04-11T07:01:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -165,17 +170,57 @@ def _base_path_parent(base_path: UserSelectionStr, s3_object: S3ObjectKey) -> st
     return f"{result}"
 
 
+def _get_project_ids(user_selecton: set[UserSelectionStr]) -> list[ProjectID]:
+    results = []
+    for selected in user_selecton:
+        project_id = ProjectID(Path(selected).parts[0])
+        results.append(project_id)
+    return results
+
+
+def _replace_node_id_project_id_in_path(
+    ids_names_map: dict[ProjectID, dict[ProjectIDStr | NodeIDStr, str]], path: str
+) -> str:
+    path_parts = Path(path).parts
+    if len(path_parts) == 0:
+        return path
+
+    if len(path_parts) == 1:
+        return ids_names_map[ProjectID(path)][path].replace("/", "_")
+
+    project_id_str = path_parts[0]
+    project_id = ProjectID(project_id_str)
+    node_id_str = path_parts[1]
+    return "/".join(
+        (
+            ids_names_map[project_id][project_id_str].replace("/", "_"),
+            ids_names_map[project_id][node_id_str].replace("/", "_"),
+            *path_parts[2:],
+        )
+    )
+
+
 async def create_and_upload_export(
-    s3_client: SimcoreS3API,
+    app: FastAPI,
path: services/storage/src/simcore_service_storage/utils/simcore_s3_dsm_utils.py
user: login: GitHK
body: I disagree with moving the part that recovers the `ids_names_map` outside of this function. The replacement logic is tied to this function so I am against such a change.
I'm OK with passing in the `ProjectRepository` and `SimcoreS3API` instances
created_at: 2025-04-11T07:06:15Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-10T13:03:43Z
body: ## [Codecov](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 68.93%. Comparing base [(`44972f0`)](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/commit/44972f012023956769ce0b8010cbda1b6ae7c91e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) to head [(`3837231`)](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/commit/3837231fd66a7a4c1bccc997026e13329b3ea8ef?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation).
> Report is 1 commits behind head on master.

> :exclamation:  There is a different number of reports uploaded between BASE (44972f0) and HEAD (3837231). Click for more details.
> 
> <details><summary>HEAD has 29 uploads less than BASE</summary>
>
>| Flag | BASE (44972f0) | HEAD (3837231) |
>|------|------|------|
>|unittests|30|1|
></details>

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           master    #7508       +/-   ##
===========================================
- Coverage   87.59%   68.93%   -18.67%     
===========================================
  Files        1752      746     -1006     
  Lines       67846    34940    -32906     
  Branches     1121      170      -951     
===========================================
- Hits        59429    24085    -35344     
- Misses       8109    10797     +2688     
+ Partials      308       58      -250     
```

| [Flag](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | Coverage Δ | |
|---|---|---|
| [integrationtests](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `65.14% <ø> (+0.01%)` | :arrow_up: |
| [unittests](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `87.51% <100.00%> (+0.73%)` | :arrow_up: |

| [Components](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=components&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | Coverage Δ | |
|---|---|---|
| [api](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [pkg_aws_library](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [pkg_dask_task_models_library](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [pkg_models_library](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [pkg_notifications_library](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [pkg_postgres_database](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [pkg_service_integration](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [pkg_service_library](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [pkg_settings_library](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [pkg_simcore_sdk](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `77.24% <ø> (-8.16%)` | :arrow_down: |
| [agent](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [api_server](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [autoscaling](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [catalog](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [clusters_keeper](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [dask_sidecar](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [datcore_adapter](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [director](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [director_v2](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `78.64% <ø> (-12.66%)` | :arrow_down: |
| [dynamic_scheduler](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [dynamic_sidecar](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `89.08% <ø> (-1.04%)` | :arrow_down: |
| [efs_guardian](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [invitations](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [payments](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [resource_usage_tracker](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [storage](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `87.51% <100.00%> (+0.04%)` | :arrow_up: |
| [webclient](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `∅ <ø> (∅)` | |
| [webserver](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation) | `59.09% <ø> (-26.89%)` | :arrow_down: |

------

[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508?dropdown=coverage&src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation). Last update [44972f0...3837231](https://app.codecov.io/gh/ITISFoundation/osparc-simcore/pull/7508?dropdown=coverage&src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ITISFoundation).

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: GitHK
created_at: 2025-04-22T12:06:12Z
body: @mergifyio queue
author_association: CONTRIBUTOR
user: login: mergify[bot]
created_at: 2025-04-22T12:06:51Z
body: > queue

#### ✅ The pull request has been merged automatically

<details>

The pull request has been merged automatically at *96bf18b304b57f70af195e686ce084e7803c949c*

</details>


<!---
DO NOT EDIT
-*- Mergify Payload -*-
{"command": "queue", "conclusion": "success", "action_is_running": true}
-*- Mergify Payload End -*-
-->
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-22T12:07:10Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=ITISFoundation_osparc-simcore&pullRequest=7508) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=ITISFoundation_osparc-simcore&pullRequest=7508&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=ITISFoundation_osparc-simcore&pullRequest=7508&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=ITISFoundation_osparc-simcore&pullRequest=7508&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=ITISFoundation_osparc-simcore&pullRequest=7508&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=ITISFoundation_osparc-simcore&pullRequest=7508&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=ITISFoundation_osparc-simcore&pullRequest=7508)


author_association: NONE